Refactor EditorBuffer and TextBuffer to be immutable

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

View File

@ -1,11 +1,11 @@
use std::path::PathBuf; use std::{path::PathBuf, rc::Rc};
use super::{CommandResponse, EditorBuffer}; use super::{CommandResult, EditorBuffer, EditorBufferState};
use crate::{Point, TextBuffer, TextBufferReader, TextBufferWriter}; use crate::{Point, TextBuffer, TextBufferReader, TextBufferWriter};
#[doc(hidden)] #[doc(hidden)]
impl EditorBuffer { impl EditorBuffer {
pub fn open_file(&mut self, filepath: PathBuf) -> CommandResponse { pub fn open_file(&self, filepath: PathBuf) -> CommandResult {
match std::fs::File::open(&filepath) { match std::fs::File::open(&filepath) {
Ok(mut file) => { Ok(mut file) => {
let mut buffer = TextBuffer::new(); let mut buffer = TextBuffer::new();
@ -15,43 +15,59 @@ impl EditorBuffer {
"Read {bytes_read} bytes from \"{}\"", "Read {bytes_read} bytes from \"{}\"",
filepath.to_string_lossy() filepath.to_string_lossy()
); );
self.filepath = Some(filepath); let state = Rc::new(EditorBufferState {
self.cursor = Point::default(); filepath: Some(filepath),
self.buffer = buffer; ..*self.state
CommandResponse::Success(msg) });
let cursor = Point::default();
let buffer = buffer;
CommandResult::success_with_message(
Self {
state,
cursor,
buffer,
..self.clone()
},
msg,
)
} }
Err(err) => CommandResponse::Failure(format!("{}", err)), Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)),
} }
} }
Err(err) => { Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound { if err.kind() == std::io::ErrorKind::NotFound {
CommandResponse::Failure(format!( CommandResult::fail_with_message(
"File not found: \"{}\"", self.clone(),
filepath.to_string_lossy() format!("File not found: \"{}\"", filepath.to_string_lossy()),
)) )
} else { } else {
CommandResponse::Failure(format!("{}", err)) CommandResult::fail_with_message(self.clone(), format!("{}", err))
} }
} }
} }
} }
pub fn save_file(&mut self, filepath: Option<PathBuf>) -> CommandResponse { pub fn save_file(&self, filepath: Option<PathBuf>) -> CommandResult {
if let Some(filepath) = filepath.as_ref().or(self.filepath.as_ref()) { if let Some(filepath) = filepath.as_ref().or(self.state.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) => CommandResponse::Success(format!( Ok(bytes_read) => CommandResult::success_with_message(
"Read {bytes_read} bytes to \"{}\"", self.clone(),
filepath.to_string_lossy() format!(
)), "Read {bytes_read} bytes to \"{}\"",
Err(err) => CommandResponse::Failure(format!("{}", err)), filepath.to_string_lossy()
),
),
Err(err) => {
CommandResult::fail_with_message(self.clone(), format!("{}", err))
}
} }
} }
Err(err) => CommandResponse::Failure(format!("{}", err)), Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)),
} }
} else { } else {
CommandResponse::Failure("Attempting to same file with no name.".into()) CommandResult::fail_with_message(self.clone(), "Attempting to same file with no name.")
} }
} }
} }
@ -71,11 +87,10 @@ mod tests {
.iter() .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 mut target = EditorBuffer::new(); let target = EditorBuffer::new();
assert!(matches!( let result = target.execute(Command::OpenFile(test_file_path));
target.execute(Command::OpenFile(test_file_path)), assert!(result.is_ok());
CommandResponse::Success(_) let target = result.buffer;
));
let mut buffer_bytes = Vec::new(); let mut buffer_bytes = Vec::new();
TextBufferReader::new(&target.buffer) TextBufferReader::new(&target.buffer)
@ -91,9 +106,11 @@ 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 mut target = EditorBuffer::new(); let target = EditorBuffer::new();
match target.execute(Command::OpenFile(test_file_path)) { let result = target.execute(Command::OpenFile(test_file_path));
CommandResponse::Failure(s) => assert_eq!(expected_message, s), assert!(!result.is_ok());
match result.response {
CommandResponse::Message(s) => assert_eq!(expected_message, s),
_ => panic!(), _ => panic!(),
} }
} }
@ -106,14 +123,12 @@ mod tests {
] ]
.iter() .iter()
.collect(); .collect();
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
target.execute(Command::OpenFile(test_file_path.clone())); let target = target.execute(Command::OpenFile(test_file_path.clone())).buffer;
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");
assert!(matches!( let result = target.execute(Command::SaveAs(tmp_file_path.clone()));
target.execute(Command::SaveAs(tmp_file_path.clone())), assert!(result.is_ok());
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();

View File

@ -1,4 +1,4 @@
use std::path::PathBuf; use std::{path::PathBuf, rc::Rc};
use crate::{Point, TextBuffer}; use crate::{Point, TextBuffer};
@ -6,26 +6,57 @@ mod command;
mod io; mod io;
pub use command::{Command, Movement, Unit}; pub use command::{Command, Movement, Unit};
#[derive(Default)] #[derive(Default, Clone)]
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, PartialEq)]
pub enum CommandResponse { pub enum CommandResponse {
Ok, None,
Success(String), Message(String),
Failure(String),
} }
impl CommandResponse { #[must_use]
pub struct CommandResult {
success: bool,
response: CommandResponse,
buffer: EditorBuffer,
}
impl CommandResult {
pub fn is_ok(&self) -> bool { pub fn is_ok(&self) -> bool {
match self { self.success
Self::Ok => true, }
Self::Success(_) => true,
Self::Failure(_) => false, fn ok(buffer: EditorBuffer) -> Self {
Self {
success: true,
response: CommandResponse::None,
buffer,
}
}
fn success_with_message(buffer: EditorBuffer, msg: impl Into<String>) -> Self {
Self {
success: true,
response: CommandResponse::Message(msg.into()),
buffer,
}
}
fn fail_with_message(buffer: EditorBuffer, msg: impl Into<String>) -> Self {
Self {
success: false,
response: CommandResponse::Message(msg.into()),
buffer,
} }
} }
} }
@ -37,7 +68,7 @@ impl EditorBuffer {
} }
/// Execute a command on the [EditorBuffer] /// Execute a command on the [EditorBuffer]
pub fn execute(&mut self, command: Command) -> CommandResponse { pub fn execute(&self, command: Command) -> CommandResult {
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),
@ -55,29 +86,34 @@ impl EditorBuffer {
self.cursor self.cursor
} }
fn move_cursor_to_point(&mut self, point: Point) -> CommandResponse { fn move_cursor_to_point(&self, point: Point) -> CommandResult {
self.cursor = point; CommandResult::ok(Self {
CommandResponse::Ok cursor: point,
..self.clone()
})
} }
fn move_cursor_to_line_number(&mut self, _line_num: usize) -> CommandResponse { fn move_cursor_to_line_number(&self, _line_num: usize) -> CommandResult {
todo!() todo!()
} }
fn insert_char(&mut self, c: char) -> CommandResponse { fn insert_char(&self, c: char) -> CommandResult {
self.buffer.insert_char(c, self.cursor); CommandResult::ok(Self {
CommandResponse::Ok buffer: self.buffer.insert_char(c, self.cursor),
cursor: self.cursor.advance(),
..self.clone()
})
} }
fn insert_string(&mut self, _s: String) -> CommandResponse { fn insert_string(&self, _s: String) -> CommandResult {
todo!() todo!()
} }
fn move_cursor(&mut self, _movement: Movement) -> CommandResponse { fn move_cursor(&self, _movement: Movement) -> CommandResult {
todo!() todo!()
} }
fn delete(&mut self, _movement: Movement) -> CommandResponse { fn delete(&self, _movement: Movement) -> CommandResult {
todo!() todo!()
} }
} }
@ -119,42 +155,38 @@ mod tests {
test_file test_file
.read_to_string(&mut expected_contents) .read_to_string(&mut expected_contents)
.expect("Reading text file"); .expect("Reading text file");
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
target.execute(Command::OpenFile(test_file.path().into())); let result = target.execute(Command::OpenFile(test_file.path().into()));
let found_contents: String = target.buffer.iter_chars().collect(); let found_contents: String = result.buffer.buffer.iter_chars().collect();
assert_eq!(expected_contents, found_contents); assert_eq!(expected_contents, found_contents);
} }
#[test] #[test]
fn cursor_at_beginning_after_file_opened() { fn cursor_at_beginning_after_file_opened() {
let test_file = create_simple_test_file(); let test_file = create_simple_test_file();
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
target.execute(Command::OpenFile(test_file.path().into())); let result = target.execute(Command::OpenFile(test_file.path().into()));
assert_eq!(Point::Start, target.get_cursor_position()); assert_eq!(Point::Start, result.buffer.get_cursor_position());
} }
#[test] #[test]
fn move_cursor_to_point_in_file() { fn move_cursor_to_point_in_file() {
let test_file = create_simple_test_file(); let test_file = create_simple_test_file();
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
target.execute(Command::OpenFile(test_file.path().into())); let result = target.execute(Command::OpenFile(test_file.path().into()));
assert!( assert!(result.is_ok());
target let target = result.buffer;
.execute(Command::MoveCursorTo(Point::LineColumn(0, 5))) let result = target.execute(Command::MoveCursorTo(Point::LineColumn(0, 5)));
.is_ok() assert!(result.is_ok());
); let target = result.buffer;
assert_eq!(Point::LineColumn(0, 5), target.get_cursor_position()); assert_eq!(Point::LineColumn(0, 5), target.get_cursor_position());
assert!( let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 11)));
target assert!(result.is_ok());
.execute(Command::MoveCursorTo(Point::LineColumn(3, 11))) let target = result.buffer;
.is_ok()
);
assert_eq!(Point::LineColumn(3, 11), target.get_cursor_position()); assert_eq!(Point::LineColumn(3, 11), target.get_cursor_position());
assert!( let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 0)));
target assert!(result.is_ok());
.execute(Command::MoveCursorTo(Point::LineColumn(3, 0))) let target = result.buffer;
.is_ok()
);
assert_eq!(Point::LineColumn(3, 0), target.get_cursor_position()); assert_eq!(Point::LineColumn(3, 0), target.get_cursor_position());
} }
@ -169,18 +201,16 @@ mod tests {
let mut expected_lines: Vec<_> = file_contents.lines().collect(); let mut expected_lines: Vec<_> = file_contents.lines().collect();
expected_lines[2] = "Xbíth a menmasam fri seilgg"; expected_lines[2] = "Xbíth a menmasam fri seilgg";
let expected_lines = expected_lines; let expected_lines = expected_lines;
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
assert!( let result = target.execute(Command::OpenFile(test_file.path().into()));
target assert!(result.is_ok());
.execute(Command::OpenFile(test_file.path().into())) let target = result.buffer;
.is_ok() let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 0)));
); assert!(result.is_ok());
assert!( let target = result.buffer;
target let result = target.execute(Command::InsertChar('X'));
.execute(Command::MoveCursorTo(Point::LineColumn(3, 0))) assert!(result.is_ok());
.is_ok() let target = result.buffer;
);
assert!(target.execute(Command::InsertChar('X')).is_ok());
let found_lines: Vec<String> = target let found_lines: Vec<String> = target
.buffer .buffer
.iter_chars() .iter_chars()
@ -199,18 +229,16 @@ mod tests {
let mut expected_lines: Vec<_> = file_contents.lines().collect(); let mut expected_lines: Vec<_> = file_contents.lines().collect();
expected_lines[2] = "bXíth a menmasam fri seilgg"; expected_lines[2] = "bXíth a menmasam fri seilgg";
let expected_lines = expected_lines; let expected_lines = expected_lines;
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
assert!( let result = target.execute(Command::OpenFile(test_file.path().into()));
target assert!(result.is_ok());
.execute(Command::OpenFile(test_file.path().into())) let target = result.buffer;
.is_ok() let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 1)));
); assert!(result.is_ok());
assert!( let target = result.buffer;
target let result = target.execute(Command::InsertChar('X'));
.execute(Command::MoveCursorTo(Point::LineColumn(3, 1))) assert!(result.is_ok());
.is_ok() let target = result.buffer;
);
assert!(target.execute(Command::InsertChar('X')).is_ok());
let found_lines: Vec<String> = target let found_lines: Vec<String> = target
.buffer .buffer
.iter_chars() .iter_chars()
@ -229,18 +257,16 @@ mod tests {
let mut expected_lines: Vec<_> = file_contents.lines().collect(); let mut expected_lines: Vec<_> = file_contents.lines().collect();
expected_lines[2] = "bíXth a menmasam fri seilgg"; expected_lines[2] = "bíXth a menmasam fri seilgg";
let expected_lines = expected_lines; let expected_lines = expected_lines;
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
assert!( let result = target.execute(Command::OpenFile(test_file.path().into()));
target assert!(result.is_ok());
.execute(Command::OpenFile(test_file.path().into())) let target = result.buffer;
.is_ok() let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 2)));
); assert!(result.is_ok());
assert!( let target = result.buffer;
target let result = target.execute(Command::InsertChar('X'));
.execute(Command::MoveCursorTo(Point::LineColumn(3, 2))) assert!(result.is_ok());
.is_ok() let target = result.buffer;
);
assert!(target.execute(Command::InsertChar('X')).is_ok());
let found_lines: Vec<String> = target let found_lines: Vec<String> = target
.buffer .buffer
.iter_chars() .iter_chars()
@ -259,18 +285,16 @@ mod tests {
let mut expected_lines: Vec<_> = file_contents.lines().collect(); let mut expected_lines: Vec<_> = file_contents.lines().collect();
expected_lines[2] = "bíth a menmXasam fri seilgg"; expected_lines[2] = "bíth a menmXasam fri seilgg";
let expected_lines = expected_lines; let expected_lines = expected_lines;
let mut target = EditorBuffer::new(); let target = EditorBuffer::new();
assert!( let result = target.execute(Command::OpenFile(test_file.path().into()));
target assert!(result.is_ok());
.execute(Command::OpenFile(test_file.path().into())) let target = result.buffer;
.is_ok() let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 11)));
); assert!(result.is_ok());
assert!( let target = result.buffer;
target let result = target.execute(Command::InsertChar('X'));
.execute(Command::MoveCursorTo(Point::LineColumn(3, 11))) assert!(result.is_ok());
.is_ok() let target = result.buffer;
);
assert!(target.execute(Command::InsertChar('X')).is_ok());
let found_lines: Vec<String> = target let found_lines: Vec<String> = target
.buffer .buffer
.iter_chars() .iter_chars()

View File

@ -11,6 +11,7 @@ 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>,
} }
@ -25,6 +26,16 @@ pub enum Point {
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 {
@ -70,8 +81,8 @@ impl TextBuffer {
} }
} }
pub fn insert_char(&mut self, c: char, point: Point) { pub fn insert_char(&self, c: char, point: Point) -> Self {
self.contents = match point { let contents = match point {
Point::Start => self.contents.insert_at_char_index(0, c), Point::Start => self.contents.insert_at_char_index(0, c),
Point::LineColumn(line_num, column_num) => { Point::LineColumn(line_num, column_num) => {
self.contents self.contents
@ -81,6 +92,7 @@ impl TextBuffer {
.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) -> bool {
@ -122,68 +134,68 @@ mod tests {
#[test] #[test]
fn insert_char_at_end_increases_counts_as_expected() { fn insert_char_at_end_increases_counts_as_expected() {
let mut target = TextBuffer::new(); let target = TextBuffer::new();
target.insert_char('A', Point::End); let target = 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());
target.insert_char(' ', Point::End); let target = 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());
target.insert_char('c', Point::End); let target = target.insert_char('c', Point::End);
assert_eq!(3, target.num_bytes()); assert_eq!(3, target.num_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());
target.insert_char('a', Point::End); let target = target.insert_char('a', Point::End);
assert_eq!(4, target.num_bytes()); assert_eq!(4, target.num_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());
target.insert_char('t', Point::End); let target = target.insert_char('t', Point::End);
assert_eq!(5, target.num_bytes()); assert_eq!(5, target.num_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());
target.insert_char('\n', Point::End); let target = target.insert_char('\n', Point::End);
assert_eq!(6, target.num_bytes()); assert_eq!(6, target.num_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());
target.insert_char('A', Point::End); let target = target.insert_char('A', Point::End);
assert_eq!(7, target.num_bytes()); assert_eq!(7, target.num_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());
target.insert_char('\n', Point::End); let target = target.insert_char('\n', Point::End);
assert_eq!(8, target.num_bytes()); assert_eq!(8, target.num_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());
target.insert_char('\n', Point::End); let target = target.insert_char('\n', Point::End);
assert_eq!(9, target.num_bytes()); assert_eq!(9, target.num_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());
target.insert_char('*', Point::End); let target = 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());
target.insert_char('*', Point::End); let target = 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());
target.insert_char(' ', Point::End); let target = 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());
target.insert_char('猫', Point::End); let target = 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());
target.insert_char(' ', Point::End); let target = 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());
target.insert_char('\n', Point::End); let target = 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());
target.insert_char('_', Point::End); let target = 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());