pub use self::cursor::Cursor;
pub use self::distance::Distance;
pub use self::gap_buffer::GapBuffer;
pub use self::line_range::LineRange;
pub use self::position::Position;
pub use self::range::Range;
pub use self::token::{Lexeme, Token, TokenSet};
pub use syntect::parsing::{Scope, ScopeStack};
mod cursor;
mod distance;
mod gap_buffer;
mod line_range;
mod operation;
mod position;
mod range;
mod token;
use self::operation::history::History;
use self::operation::{Operation, OperationGroup};
use crate::errors::*;
use std::cell::RefCell;
use std::default::Default;
use std::fs::{self, File};
use std::io;
use std::io::Write;
use std::ops::Fn;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use syntect::parsing::SyntaxReference;
pub struct Buffer {
pub id: Option<usize>,
data: Rc<RefCell<GapBuffer>>,
pub path: Option<PathBuf>,
pub cursor: Cursor,
history: History,
operation_group: Option<OperationGroup>,
pub syntax_definition: Option<SyntaxReference>,
pub change_callback: Option<Box<dyn Fn(Position)>>,
}
impl Default for Buffer {
fn default() -> Self {
let data = Rc::new(RefCell::new(GapBuffer::new(String::new())));
let cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 });
let mut history = History::new();
history.mark();
Buffer {
id: None,
data: data.clone(),
path: None,
cursor,
history: History::new(),
operation_group: None,
syntax_definition: None,
change_callback: None,
}
}
}
impl Buffer {
pub fn new() -> Buffer {
Default::default()
}
pub fn from_file(path: &Path) -> io::Result<Buffer> {
let content = fs::read_to_string(path)?;
let data = Rc::new(RefCell::new(GapBuffer::new(content)));
let cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 });
let mut buffer = Buffer {
id: None,
data: data.clone(),
path: Some(path.canonicalize()?),
cursor,
history: History::new(),
operation_group: None,
syntax_definition: None,
change_callback: None,
};
buffer.history.mark();
Ok(buffer)
}
pub fn data(&self) -> String {
self.data.borrow().to_string()
}
pub fn save(&mut self) -> io::Result<()> {
let mut file = if let Some(ref path) = self.path {
File::create(path)?
} else {
File::create(PathBuf::new())?
};
file.write_all(self.data().to_string().as_bytes())?;
self.history.mark();
Ok(())
}
pub fn file_name(&self) -> Option<String> {
self.path.as_ref().and_then(|p| {
p.file_name()
.and_then(|f| f.to_str().map(|s| s.to_string()))
})
}
pub fn undo(&mut self) {
let operation: Option<Box<dyn Operation>> = match self.operation_group.take() {
Some(group) => {
if group.is_empty() {
self.history.previous()
} else {
Some(Box::new(group))
}
}
None => self.history.previous(),
};
if let Some(mut op) = operation {
op.reverse(self);
}
}
pub fn redo(&mut self) {
if let Some(mut op) = self.history.next() {
op.run(self);
}
}
pub fn read(&self, range: &Range) -> Option<String> {
self.data.borrow().read(range)
}
pub fn search(&self, needle: &str) -> Vec<Position> {
let mut results = Vec::new();
for (line, data) in self.data().lines().enumerate() {
for (offset, _) in data.char_indices() {
let haystack = &data[offset..];
if haystack.len() >= needle.len()
&& needle.as_bytes() == &haystack.as_bytes()[..needle.len()]
{
results.push(Position { line, offset });
}
}
}
results
}
pub fn modified(&self) -> bool {
!self.history.at_mark()
}
pub fn line_count(&self) -> usize {
self.data().chars().filter(|&c| c == '\n').count() + 1
}
pub fn reload(&mut self) -> Result<()> {
let path = self.path.as_ref().ok_or(Error::MissingPath)?;
let content = fs::read_to_string(path)?;
self.replace(content);
self.history.mark();
Ok(())
}
pub fn file_extension(&self) -> Option<String> {
self.path.as_ref().and_then(|p| {
p.extension().and_then(|e| {
if !e.is_empty() {
return Some(e.to_string_lossy().into_owned());
}
None
})
})
}
}
#[cfg(test)]
mod tests {
extern crate syntect;
use crate::buffer::{Buffer, Position};
use crate::Error;
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use syntect::parsing::SyntaxSet;
#[test]
fn reload_returns_missing_path_error_when_path_is_unset() {
let mut buffer = Buffer::new();
let error = buffer.reload().unwrap_err();
assert!(matches!(error, Error::MissingPath));
}
#[test]
fn reload_persists_id_and_syntax_definition() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
let syntax_set = SyntaxSet::load_defaults_newlines();
let syntax_definition = Some(syntax_set.find_syntax_plain_text().clone());
buffer.id = Some(1);
buffer.syntax_definition = syntax_definition;
buffer.reload().unwrap();
assert_eq!(buffer.id, Some(1));
assert!(buffer.syntax_definition.is_some());
}
#[test]
fn reload_retains_full_position_when_possible() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.cursor.move_to(Position { line: 0, offset: 3 });
buffer.reload().unwrap();
assert_eq!(*buffer.cursor, Position { line: 0, offset: 3 });
}
#[test]
fn reload_retains_position_line_when_possible() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.insert("amp\neditor");
buffer.cursor.move_to(Position { line: 1, offset: 1 });
buffer.reload().unwrap();
assert_eq!(*buffer.cursor, Position { line: 1, offset: 0 });
}
#[test]
fn reload_discards_position_when_impossible() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.insert("\namp\neditor");
buffer.cursor.move_to(Position { line: 2, offset: 1 });
buffer.reload().unwrap();
assert_eq!(*buffer.cursor, Position::new());
}
#[test]
fn reload_calls_change_callback_with_zero_position() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.insert("amp\neditor");
let tracked_position = Rc::new(RefCell::new(Position { line: 1, offset: 1 }));
let callback_position = tracked_position.clone();
buffer.change_callback = Some(Box::new(move |change_position| {
*callback_position.borrow_mut() = change_position
}));
buffer.reload().unwrap();
assert_eq!(*tracked_position.borrow(), Position::new());
}
#[test]
fn reload_marks_buffer_as_unmodified() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.insert("amp\neditor");
buffer.reload().unwrap();
assert!(!buffer.modified());
}
#[test]
fn reload_retains_history() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
buffer.insert("amp\neditor");
buffer.reload().unwrap();
assert!(buffer.history.previous().is_some());
}
#[test]
fn delete_joins_lines_when_invoked_at_end_of_line() {
let mut buffer = Buffer::new();
buffer.insert("scribe\n library");
buffer.cursor.move_to_end_of_line();
buffer.delete();
assert_eq!(buffer.data(), "scribe library");
}
#[test]
fn delete_does_nothing_when_invoked_at_the_end_of_the_document() {
let mut buffer = Buffer::new();
buffer.insert("scribe\n library");
buffer.cursor.move_down();
buffer.cursor.move_to_end_of_line();
buffer.delete();
assert_eq!(buffer.data(), "scribe\n library");
}
#[test]
fn insert_is_undoable() {
let mut buffer = Buffer::new();
buffer.insert("scribe");
assert_eq!("scribe", buffer.data());
buffer.undo();
assert_eq!("", buffer.data());
}
#[test]
fn delete_is_undoable() {
let mut buffer = Buffer::new();
buffer.insert("scribe");
assert_eq!("scribe", buffer.data());
buffer.cursor.move_to(Position { line: 0, offset: 0 });
buffer.delete();
assert_eq!("cribe", buffer.data());
buffer.undo();
assert_eq!("scribe", buffer.data());
}
#[test]
fn correctly_called_operation_groups_are_undone_correctly() {
let mut buffer = Buffer::new();
buffer.start_operation_group();
buffer.insert("scribe");
buffer.cursor.move_to(Position { line: 0, offset: 6 });
buffer.insert(" library");
buffer.end_operation_group();
buffer.cursor.move_to(Position {
line: 0,
offset: 14,
});
buffer.insert(" test");
assert_eq!("scribe library test", buffer.data());
buffer.undo();
assert_eq!("scribe library", buffer.data());
buffer.undo();
assert_eq!("", buffer.data());
}
#[test]
fn non_terminated_operation_groups_are_undone_correctly() {
let mut buffer = Buffer::new();
buffer.insert("scribe");
buffer.start_operation_group();
buffer.cursor.move_to(Position { line: 0, offset: 6 });
buffer.insert(" library");
buffer.cursor.move_to(Position {
line: 0,
offset: 14,
});
buffer.insert(" test");
assert_eq!("scribe library test", buffer.data());
buffer.undo();
assert_eq!("scribe", buffer.data());
buffer.undo();
assert_eq!("", buffer.data());
}
#[test]
fn non_terminated_empty_operation_groups_are_dropped() {
let mut buffer = Buffer::new();
buffer.insert("scribe");
buffer.start_operation_group();
buffer.undo();
assert_eq!(buffer.data(), "");
}
#[test]
fn search_returns_empty_set_when_there_are_no_matches() {
let mut buffer = Buffer::new();
buffer.insert("scribe");
assert!(buffer.search("library").is_empty());
}
#[test]
fn search_does_not_panic_with_non_ascii_data() {
let mut buffer = Buffer::new();
buffer.insert("scribé");
assert!(buffer.search("library").is_empty());
assert!(buffer.search("scribe").is_empty());
assert!(buffer.search("scribé").len() > 0);
}
}