use ropey::Rope;
use super::change::Change;
use super::cursor::Selection;
use super::decorations::DecorationCollections;
use super::history::History;
use super::rope_ext::RopeExt as _;
use crate::sum_tree::Bias;
use std::ops::Range;
pub struct TextCore {
pub(super) text: Rope,
pub(super) selected_range: Selection,
pub(super) selection_reversed: bool,
pub(super) history: History<Change>,
pub(super) decorations: DecorationCollections,
pub(super) ime_marked_range: Option<Selection>,
}
impl TextCore {
pub(super) fn new() -> Self {
Self {
text: "".into(),
selected_range: Selection::default(),
selection_reversed: false,
history: History::new().group_interval(std::time::Duration::from_secs(1)),
decorations: DecorationCollections::default(),
ime_marked_range: None,
}
}
pub(super) fn cursor(&self) -> usize {
if let Some(ime_marked_range) = &self.ime_marked_range {
return ime_marked_range.end;
}
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
pub(super) fn push_history(&mut self, old_text: &Rope, range: &Range<usize>, new_text: &str) {
if self.history.ignore {
return;
}
let range = old_text.clip_offset(range.start, Bias::Left)
..old_text.clip_offset(range.end, Bias::Right);
let old_snippet = old_text.slice(range.clone()).to_string();
let new_range = range.start..range.start + new_text.len();
self.history
.push(Change::new(range, &old_snippet, new_range, new_text));
}
pub(super) fn apply_replace(&mut self, range: Range<usize>, new_text: &str) -> usize {
let old_text = self.text.clone();
self.text.replace(range.clone(), new_text);
let new_offset = (range.start + new_text.len()).min(self.text.len());
self.decorations.adjust_for_edit(&range, new_text.len());
self.push_history(&old_text, &range, new_text);
self.history.end_grouping();
self.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take();
new_offset
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_core_defaults() {
let core = TextCore::new();
assert_eq!(core.text.len(), 0);
assert!(core.selected_range.is_empty());
assert_eq!(core.cursor(), 0);
assert!(core.ime_marked_range.is_none());
}
#[test]
fn cursor_semantics() {
let mut core = TextCore::new();
core.selected_range = Selection::new(2, 5);
assert_eq!(core.cursor(), 5);
core.selection_reversed = true;
assert_eq!(core.cursor(), 2);
core.ime_marked_range = Some(Selection::new(1, 4));
assert_eq!(core.cursor(), 4);
}
#[test]
fn apply_replace_basic() {
let mut core = TextCore::new();
core.text = Rope::from("hello world");
let new_offset = core.apply_replace(6..11, "rgpui");
assert_eq!(core.text.to_string(), "hello rgpui");
assert_eq!(new_offset, 11);
assert_eq!(core.cursor(), 11);
assert!(core.ime_marked_range.is_none());
let changes = core.history.undo().expect("has undo");
assert_eq!(changes.len(), 1);
let change = &changes[0];
let mut text = core.text.clone();
let range = change.new_range.start..change.new_range.end;
text.replace(range, &change.old_text);
assert_eq!(text.to_string(), "hello world");
}
#[test]
fn push_history_respects_ignore() {
let mut core = TextCore::new();
core.text = Rope::from("abc");
core.history.ignore = true;
core.push_history(&core.text.clone(), &(1..2), "X");
assert!(core.history.undo().is_none());
core.history.ignore = false;
core.push_history(&core.text.clone(), &(1..2), "X");
assert!(core.history.undo().is_some());
}
}