pub mod graphemes;
pub mod movement;
pub mod tree;
mod diff;
use ropey::{Rope, RopeSlice};
use std::{cmp, ops::Range};
pub use self::{
diff::{DeleteOperation, OpaqueDiff},
graphemes::{CharIndex, RopeExt, RopeGraphemes},
movement::Direction,
};
pub const TAB_WIDTH: usize = 4;
trait RopeCursorExt {
fn cursor_to_line(&self, cursor: &Cursor) -> usize;
fn slice_cursor(&self, cursor: &Cursor) -> RopeSlice;
}
impl RopeCursorExt for Rope {
fn cursor_to_line(&self, cursor: &Cursor) -> usize {
self.char_to_line(cursor.range.start)
}
fn slice_cursor(&self, cursor: &Cursor) -> RopeSlice {
self.slice(cursor.range.start..cursor.range.end)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Cursor {
range: Range<CharIndex>,
selection: Option<CharIndex>,
visual_horizontal_offset: Option<usize>,
}
impl Default for Cursor {
fn default() -> Self {
Self::new()
}
}
impl Cursor {
pub fn new() -> Self {
Self {
range: 0..0,
selection: None,
visual_horizontal_offset: None,
}
}
pub fn with_range(range: Range<CharIndex>) -> Self {
Self {
range,
..Self::new()
}
}
#[cfg(test)]
pub fn end_of_buffer(text: &Rope) -> Self {
Self {
range: text.prev_grapheme_boundary(text.len_chars())..text.len_chars(),
visual_horizontal_offset: None,
selection: None,
}
}
pub fn is_empty(&self) -> bool {
self.range.is_empty()
}
pub fn range(&self) -> Range<CharIndex> {
self.range.clone()
}
pub fn selection(&self) -> Range<CharIndex> {
match self.selection {
Some(selection) if selection > self.range.start => self.range.start..selection,
Some(selection) if selection < self.range.start => selection..self.range.start,
_ => self.range.clone(),
}
}
pub fn column_offset(&self, text: &Rope) -> usize {
let char_line_start = text.line_to_char(text.cursor_to_line(self));
graphemes::width(&text.slice(char_line_start..self.range.start))
}
pub fn reconcile(&mut self, new_text: &Rope, diff: &OpaqueDiff) {
let OpaqueDiff {
char_index,
old_char_length,
new_char_length,
..
} = *diff;
let modified_range = char_index..cmp::max(old_char_length, new_char_length);
if modified_range.start >= self.range.end {
return;
}
if modified_range.end <= self.range.start {
let (start, end) = (self.range.start, self.range.end);
if old_char_length > new_char_length {
let length_change = old_char_length - new_char_length;
self.range = start.saturating_sub(length_change)..end.saturating_sub(length_change);
} else {
let length_change = new_char_length - old_char_length;
self.range = start + length_change..end + length_change;
};
}
let grapheme_start =
new_text.prev_grapheme_boundary(cmp::min(self.range.end, new_text.len_chars()));
let grapheme_end = new_text.next_grapheme_boundary(grapheme_start);
self.range = grapheme_start..grapheme_end
}
pub fn begin_selection(&mut self) {
self.selection = Some(self.range.start)
}
pub fn clear_selection(&mut self) {
self.selection = None;
}
pub fn select_all(&mut self, text: &Rope) {
movement::move_to_start_of_buffer(text, self);
self.selection = Some(text.len_chars());
}
pub fn insert_char(&mut self, text: &mut Rope, character: char) -> OpaqueDiff {
self.clear_selection();
text.insert_char(self.range.start, character);
OpaqueDiff::new(
text.char_to_byte(self.range.start),
0,
character.len_utf8(),
self.range.start,
0,
1,
)
}
pub fn insert_chars(
&mut self,
text: &mut Rope,
characters: impl IntoIterator<Item = char>,
) -> OpaqueDiff {
self.clear_selection();
let mut num_bytes = 0;
let mut num_chars = 0;
characters
.into_iter()
.enumerate()
.for_each(|(offset, character)| {
text.insert_char(self.range.start + offset, character);
num_bytes += character.len_utf8();
num_chars += 1;
});
OpaqueDiff::new(
text.char_to_byte(self.range.start),
0,
num_bytes,
self.range.start,
0,
num_chars,
)
}
pub fn delete_forward(&mut self, text: &mut Rope) -> DeleteOperation {
if text.len_chars() == 0 || text.len_chars() == self.range.start {
return DeleteOperation::empty();
}
let byte_range = text.char_to_byte(self.range.start)..text.char_to_byte(self.range.end);
let diff = OpaqueDiff::new(
byte_range.start,
byte_range.end - byte_range.start,
0,
self.range.start,
self.range.end - self.range.start,
0,
);
text.remove(self.range.clone());
let grapheme_start = self.range.start;
let grapheme_end = text.next_grapheme_boundary(self.range.start);
let deleted = text.slice(grapheme_start..grapheme_end).into();
*self = Cursor::with_range(grapheme_start..grapheme_end);
DeleteOperation { diff, deleted }
}
pub fn delete_backward(&mut self, text: &mut Rope) -> DeleteOperation {
if self.range.start > 0 {
movement::move_horizontally(text, self, Direction::Backward, 1);
self.delete_forward(text)
} else {
DeleteOperation::empty()
}
}
pub fn delete_line(&mut self, text: &mut Rope) -> DeleteOperation {
if text.len_chars() == 0 {
return DeleteOperation::empty();
}
let line_index = text.char_to_line(self.range.start);
let delete_range_start = text.line_to_char(line_index);
let delete_range_end = text.line_to_char(line_index + 1);
let deleted = text.slice(delete_range_start..delete_range_end).into();
let diff = OpaqueDiff::new(
text.char_to_byte(delete_range_start),
text.char_to_byte(delete_range_end) - text.char_to_byte(delete_range_start),
0,
delete_range_start,
delete_range_end - delete_range_start,
0,
);
text.remove(delete_range_start..delete_range_end);
let grapheme_start =
text.line_to_char(cmp::min(line_index, text.len_lines().saturating_sub(2)));
let grapheme_end = text.next_grapheme_boundary(grapheme_start);
*self = Cursor::with_range(grapheme_start..grapheme_end);
DeleteOperation { diff, deleted }
}
pub fn delete_selection(&mut self, text: &mut Rope) -> DeleteOperation {
if text.len_chars() == 0 {
return DeleteOperation::empty();
}
let selection = self.selection();
let deleted = text.slice(selection.start..selection.end).into();
let diff = OpaqueDiff::new(
text.char_to_byte(selection.start),
text.char_to_byte(selection.end) - text.char_to_byte(selection.start),
0,
selection.start,
selection.end - selection.start,
0,
);
text.remove(selection.start..selection.end);
let grapheme_start = cmp::min(
self.range.start,
text.prev_grapheme_boundary(text.len_chars()),
);
let grapheme_end = text.next_grapheme_boundary(grapheme_start);
*self = Cursor::with_range(grapheme_start..grapheme_end);
DeleteOperation { diff, deleted }
}
pub fn sync(&mut self, current_text: &Rope, new_text: &Rope) {
let current_line = current_text.char_to_line(self.range.start);
let current_line_offset = self.range.start - current_text.line_to_char(current_line);
let new_line = cmp::min(current_line, new_text.len_lines().saturating_sub(1));
let new_line_offset = cmp::min(
current_line_offset,
new_text.line(new_line).len_chars().saturating_sub(1),
);
let grapheme_end =
new_text.next_grapheme_boundary(new_text.line_to_char(new_line) + new_line_offset);
let grapheme_start = new_text.prev_grapheme_boundary(grapheme_end);
*self = Cursor::with_range(grapheme_start..grapheme_end);
}
}
#[cfg(test)]
mod tests {
use ropey::Rope;
use super::*;
fn text_with_cursor(text: impl Into<Rope>) -> (Rope, Cursor) {
let text = text.into();
let mut cursor = Cursor::new();
movement::move_horizontally(&text, &mut cursor, Direction::Backward, 1);
(text, cursor)
}
#[test]
fn sync_with_empty() {
let current_text = Rope::from("Buy a milk goat\nAt the market\n");
let new_text = Rope::from("");
let mut cursor = Cursor::new();
movement::move_horizontally(¤t_text, &mut cursor, Direction::Forward, 4);
cursor.sync(¤t_text, &new_text);
assert_eq!(Cursor::new(), cursor);
}
#[test]
fn delete_forward_at_the_end() {
let (mut text, mut cursor) = text_with_cursor(TEXT);
let expected = text.clone();
movement::move_to_end_of_buffer(&text, &mut cursor);
cursor.delete_forward(&mut text);
assert_eq!(expected, text);
}
#[test]
fn delete_forward_empty_text() {
let (mut text, mut cursor) = text_with_cursor("");
cursor.delete_forward(&mut text);
assert_eq!(cursor, Cursor::new());
}
#[test]
fn delete_forward_at_the_begining() {
let (mut text, mut cursor) = text_with_cursor("// Hello world!\n\n");
let expected = Rope::from("Hello world!\n\n");
cursor.delete_forward(&mut text);
cursor.delete_forward(&mut text);
cursor.delete_forward(&mut text);
assert_eq!(expected, text);
}
#[test]
fn delete_backward_at_the_end() {
let (mut text, mut cursor) = text_with_cursor("// Hello world!\n");
movement::move_to_end_of_buffer(&text, &mut cursor);
cursor.delete_backward(&mut text);
assert_eq!(Rope::from("// Hello world!"), text);
cursor.delete_backward(&mut text);
assert_eq!(Rope::from("// Hello world"), text);
}
#[test]
fn delete_backward_empty_text() {
let (mut text, mut cursor) = text_with_cursor("");
cursor.delete_backward(&mut text);
assert_eq!(cursor, Cursor::new());
}
#[test]
fn delete_backward_at_the_begining() {
let (mut text, mut cursor) = text_with_cursor("// Hello world!\n");
let expected = text.clone();
cursor.delete_backward(&mut text);
assert_eq!(expected, text);
}
const TEXT: &str = r#"
Basic Latin
! " # $ % & ' ( ) *+,-./012ABCDEFGHI` a m t u v z { | } ~
CJK
豈 更 車 Ⅷ
"#;
}