use crate::ropetext::motion::{self, Goal, Words};
use crate::ropetext::{Change, Column, EditBuffer as Rope, Position, Span, Text};
const DEFAULT_INDENT_WIDTH: u8 = 4;
#[must_use = "an edit's outcome drives the revision bump and the parse-damage signal"]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EditOutcome {
pub changed: bool,
pub bulk: bool,
pub damage: Option<std::ops::Range<usize>>,
pub line_delta: isize,
}
pub(super) fn shift_rows(
range: std::ops::Range<usize>,
at: usize,
delta: isize,
) -> std::ops::Range<usize> {
let shift = |row: usize| {
if row < at {
row
} else {
row.saturating_add_signed(delta)
}
};
shift(range.start)..shift(range.end)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Yank {
Keep,
Discard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorMove {
Forward,
Back,
Up,
Down,
Head,
End,
Top,
Bottom,
WordForward,
WordBack,
WordEnd,
WordForwardBig,
WordBackBig,
WordEndBig,
WordEndBack {
big: bool,
},
MatchingPair,
ParagraphForward,
ParagraphBack,
Jump(usize, usize),
}
impl CursorMove {
fn is_vertical(self) -> bool {
matches!(self, CursorMove::Up | CursorMove::Down)
}
}
#[derive(Debug)]
pub struct RopeBuffer {
inner: Rope,
pending: EditOutcome,
depth: u32,
group_started: bool,
continue_group: bool,
goal: Option<Column>,
yank: String,
search: Option<regex::Regex>,
indent_width: u8,
hard_tab_indent: bool,
}
impl Default for RopeBuffer {
fn default() -> Self {
Self::new(Text::new())
}
}
impl RopeBuffer {
pub fn new(text: Text) -> Self {
Self {
inner: Rope::new(text),
pending: EditOutcome::default(),
depth: 0,
group_started: false,
continue_group: false,
goal: None,
yank: String::new(),
search: None,
indent_width: DEFAULT_INDENT_WIDTH,
hard_tab_indent: false,
}
}
pub fn replace(&mut self, text: Text) {
self.inner.set_text(text);
self.pending = EditOutcome::default();
self.goal = None;
}
pub fn text(&self) -> &Text {
self.inner.text()
}
pub fn indent_width(&self) -> u8 {
self.indent_width
}
pub fn set_indent_width(&mut self, spaces: u8) {
self.indent_width = spaces;
}
pub fn hard_tab_indent(&self) -> bool {
self.hard_tab_indent
}
pub fn set_hard_tab_indent(&mut self, hard: bool) {
self.hard_tab_indent = hard;
}
pub fn snapshot(&self) -> crate::ropetext::Snapshot {
self.inner.snapshot()
}
pub fn row(&self, row: usize) -> Option<std::borrow::Cow<'_, str>> {
self.inner.text().line(row)
}
pub fn row_count(&self) -> usize {
self.inner.text().line_count()
}
pub fn rows(&self) -> Vec<String> {
self.inner.text().lines().map(|l| l.to_string()).collect()
}
pub fn joined_rows(&self, first: usize, last: usize) -> String {
(first..=last)
.filter_map(|row| self.row(row))
.collect::<Vec<_>>()
.join("\n")
}
pub fn row_len(&self, row: usize) -> usize {
self.inner.text().line_len_chars(row).unwrap_or(0)
}
pub fn cursor(&self) -> (usize, usize) {
let cursor = self.inner.cursor();
(cursor.row(), cursor.column().get())
}
pub fn is_empty(&self) -> bool {
self.inner.text().len_bytes() == 0
}
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
let span = self.inner.selection()?;
Some((rc(span.start()), rc(span.end())))
}
pub fn yank_text(&self) -> String {
self.yank.clone()
}
pub fn set_yank_text(&mut self, text: impl Into<String>) {
self.yank = text.into();
}
pub fn search_pattern(&self) -> Option<®ex::Regex> {
self.search.as_ref()
}
pub fn take_outcome(&mut self) -> EditOutcome {
std::mem::take(&mut self.pending)
}
pub fn edit<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
if self.depth > 0 {
return f(self);
}
self.depth = 1;
self.group_started = false;
let out = f(self);
self.depth = 0;
self.group_started = false;
out
}
pub fn continue_group(&mut self) {
self.continue_group = true;
}
fn mutate(&mut self, f: impl FnOnce(&mut crate::ropetext::Txn<'_>)) -> bool {
let extending =
(self.depth > 0 && self.group_started) || std::mem::take(&mut self.continue_group);
let mut txn = if extending {
self.inner.begin_extending()
} else {
self.inner.begin()
};
f(&mut txn);
let change = txn.commit();
if self.depth > 0 {
self.group_started = true;
}
self.record(change)
}
fn record(&mut self, change: Option<Change>) -> bool {
let Some(change) = change else {
return false;
};
self.pending.changed = true;
self.pending.bulk |= change.is_bulk();
self.pending.line_delta += change.line_delta();
self.pending.damage = Some(match self.pending.damage.take() {
Some(seen) => {
let seen = shift_rows(seen, change.rows().start, change.line_delta());
seen.start.min(change.rows().start)..seen.end.max(change.rows().end)
}
None => change.rows(),
});
true
}
pub fn insert_str(&mut self, s: impl AsRef<str>) -> bool {
let text = s.as_ref().to_string();
let span = self.inner.selection().filter(|span| !span.is_empty());
self.inner.clear_selection();
let cursor = self.inner.cursor();
self.goal = None;
self.mutate(|txn| match span {
Some(span) => {
txn.replace(span, &text);
}
None => {
txn.insert(cursor, &text);
}
})
}
pub fn insert_char(&mut self, c: char) {
self.insert_str(c.to_string());
}
pub fn insert_newline(&mut self) {
self.insert_str("\n");
}
pub fn delete_str(&mut self, clusters: usize) -> bool {
if self.take_selection() {
return true;
}
if clusters == 0 {
return false;
}
let from = self.inner.cursor();
let to = self.forward_by(from, clusters);
self.delete_between(from, to, Yank::Keep)
}
pub fn delete_char(&mut self) -> bool {
if self.take_selection() {
return true;
}
let to = self.inner.cursor();
let from = motion::prev_cluster(self.inner.text(), to);
self.delete_between(from, to, Yank::Discard)
}
pub fn delete_next_char(&mut self) -> bool {
if self.take_selection() {
return true;
}
let from = self.inner.cursor();
let to = motion::next_cluster(self.inner.text(), from);
self.delete_between(from, to, Yank::Discard)
}
pub fn delete_word(&mut self) -> bool {
if self.take_selection() {
return true;
}
let to = self.inner.cursor();
let text = self.inner.text();
let candidate = motion::word_start_back(text, to, Words::Small);
let (from, yank) = if candidate.row() == to.row() && candidate.byte() < to.byte() {
(candidate, Yank::Keep)
} else if to.column().get() > 0 {
(motion::row_start(text, to), Yank::Keep)
} else {
(motion::prev_cluster(text, to), Yank::Discard)
};
self.delete_between(from, to, yank)
}
pub fn delete_next_word(&mut self) -> bool {
if self.take_selection() {
return true;
}
let from = self.inner.cursor();
let text = self.inner.text();
let candidate = motion::word_end_at_or_after(text, from, Words::Small);
let row_end = motion::row_end(text, from);
let (to, yank) = match candidate {
Some(end) if end.row() == from.row() && end.byte() > from.byte() => (end, Yank::Keep),
_ if from.byte() < row_end.byte() => (row_end, Yank::Keep),
_ => (motion::next_cluster(text, from), Yank::Discard),
};
self.delete_between(from, to, yank)
}
pub fn cut(&mut self) -> bool {
let span = self.inner.selection().filter(|span| !span.is_empty());
self.inner.clear_selection();
let Some(span) = span else {
return false;
};
self.yank = self
.inner
.text()
.slice(span)
.map(|text| text.to_string())
.unwrap_or_default();
self.goal = None;
self.mutate(|txn| {
txn.delete(span);
})
}
pub fn copy(&mut self) {
if let Some(span) = self.inner.selection().filter(|span| !span.is_empty())
&& let Some(text) = self.inner.text().slice(span)
{
self.yank = text.to_string();
}
}
pub fn paste(&mut self) -> bool {
if self.yank.is_empty() {
return false;
}
let text = std::mem::take(&mut self.yank);
let changed = self.insert_str(&text);
self.yank = text;
changed
}
fn take_selection(&mut self) -> bool {
let span = self.inner.selection().filter(|span| !span.is_empty());
self.inner.clear_selection();
let Some(span) = span else {
return false;
};
self.delete_between(span.start(), span.end(), Yank::Discard)
}
fn delete_between(&mut self, from: Position, to: Position, yank: Yank) -> bool {
let Some(span) = self.inner.text().span(from, to) else {
return false;
};
if span.is_empty() {
return false;
}
if yank == Yank::Keep
&& let Some(text) = self.inner.text().slice(span)
{
self.yank = text.to_string();
}
self.goal = None;
self.mutate(|txn| {
txn.delete(span);
})
}
pub fn undo(&mut self) -> bool {
let change = self.inner.undo();
self.after_history(change)
}
pub fn redo(&mut self) -> bool {
let change = self.inner.redo();
self.after_history(change)
}
fn after_history(&mut self, change: Option<Change>) -> bool {
if change.is_none() {
return false;
}
self.goal = None;
self.inner.clear_selection();
self.record(change)
}
pub fn move_cursor(&mut self, movement: CursorMove) {
let text = self.inner.text();
let from = self.inner.cursor();
let goal = self
.goal
.filter(|_| movement.is_vertical())
.unwrap_or_else(|| from.column());
let to = match movement {
CursorMove::Forward => motion::next_cluster(text, from),
CursorMove::Back => motion::prev_cluster(text, from),
CursorMove::Up => motion::vertical(text, from, -1, Goal::Column(goal)),
CursorMove::Down => motion::vertical(text, from, 1, Goal::Column(goal)),
CursorMove::Head => motion::row_start(text, from),
CursorMove::End => motion::row_end(text, from),
CursorMove::Top => {
let up = -(from.row() as isize);
motion::vertical(text, from, up, Goal::Column(goal))
}
CursorMove::Bottom => {
let down = (text.line_count().saturating_sub(1) as isize) - from.row() as isize;
motion::vertical(text, from, down, Goal::Column(goal))
}
CursorMove::WordForward => motion::word_start_forward(text, from, Words::Small),
CursorMove::WordBack => motion::word_start_back(text, from, Words::Small),
CursorMove::WordForwardBig => motion::word_start_forward(text, from, Words::Big),
CursorMove::WordBackBig => motion::word_start_back(text, from, Words::Big),
CursorMove::WordEndBig => match motion::word_end_forward(text, from, Words::Big) {
Some(end) => motion::prev_cluster(text, end),
None => from,
},
CursorMove::WordEndBack { big } => {
let words = if big { Words::Big } else { Words::Small };
match motion::word_end_back(text, from, words) {
Some(end) => motion::prev_cluster(text, end),
None => from,
}
}
CursorMove::MatchingPair => motion::matching_bracket(text, from).unwrap_or(from),
CursorMove::WordEnd => match motion::word_end_forward(text, from, Words::Small) {
Some(end) => motion::prev_cluster(text, end),
None => motion::text_end(text),
},
CursorMove::ParagraphForward => motion::paragraph_forward(text, from),
CursorMove::ParagraphBack => motion::paragraph_back(text, from),
CursorMove::Jump(row, column) => {
match text.position(row, Column::new(column)) {
Some(position) => position,
None => return,
}
}
};
self.goal = if movement.is_vertical() {
Some(goal)
} else {
None
};
self.place(to);
}
pub fn jump_to(&mut self, row: usize, col: usize) -> bool {
let Some(to) = self.inner.text().position(row, Column::new(col)) else {
return false;
};
self.goal = None;
self.place(to);
true
}
pub fn move_to(&mut self, to: Position) {
if self.inner.text().is_stale(to) {
return;
}
self.goal = None;
self.place(to);
}
fn place(&mut self, to: Position) {
if self.inner.selection().is_some() {
self.inner.extend_to(to);
} else {
self.inner.set_cursor(to);
}
}
pub fn start_selection(&mut self) {
let cursor = self.inner.cursor();
self.inner.clear_selection();
self.inner.extend_to(cursor);
}
pub fn cancel_selection(&mut self) {
self.inner.clear_selection();
}
pub fn select_all(&mut self) {
let span = self.inner.text().full_span();
self.inner.select(span);
}
pub fn set_selection(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
let text = self.inner.text();
let Some(from) = text.position(start.0, Column::new(start.1)) else {
return false;
};
let Some(to) = text.position(end.0, Column::new(end.1)) else {
return false;
};
let Some(span) = text.span(from, to) else {
return false;
};
self.inner.select(span);
true
}
pub fn set_search_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
if pattern.is_empty() {
self.search = None;
return Ok(());
}
self.search = Some(regex::Regex::new(pattern)?);
Ok(())
}
pub fn search_forward(&mut self, match_cursor: bool) -> bool {
self.step_search(false, match_cursor)
}
pub fn search_back(&mut self, match_cursor: bool) -> bool {
self.step_search(true, match_cursor)
}
pub fn search_repeat(&mut self, backward: bool) -> bool {
self.step_search(backward, false)
}
fn step_search(&mut self, backward: bool, match_cursor: bool) -> bool {
self.cancel_selection();
let Some(found) = self.find_match(backward, match_cursor) else {
return false;
};
self.goal = None;
self.inner.set_cursor(found);
true
}
fn find_match(&self, backward: bool, match_cursor: bool) -> Option<Position> {
let pattern = self.search.as_ref()?;
let text = self.inner.text();
let cursor = self.inner.cursor();
let rows = text.line_count();
for step in 0..=rows {
let wrapped = step == rows;
let row = if backward {
(cursor.row() + rows - (step % rows.max(1))) % rows
} else {
(cursor.row() + step) % rows
};
let line = text.line(row)?;
let mut hits: Vec<usize> = pattern
.find_iter(&line)
.map(|found| line[..found.start()].chars().count())
.collect();
if backward {
hits.reverse();
}
for column in hits {
let same_row = row == cursor.row();
let beyond = if backward {
column < cursor.column().get()
} else if match_cursor {
column >= cursor.column().get()
} else {
column > cursor.column().get()
};
if wrapped || !same_row || beyond {
if let Some(at) = text.position(row, Column::new(column)) {
return Some(at);
}
}
}
}
None
}
pub fn match_at_cursor(&self) -> Option<((usize, usize), (usize, usize))> {
let pattern = self.search.as_ref()?;
let text = self.inner.text();
let cursor = self.inner.cursor();
let line = text.line(cursor.row())?;
let byte = line
.char_indices()
.nth(cursor.column().get())
.map(|(at, _)| at)
.unwrap_or(line.len());
let found = pattern.find_at(&line, byte)?;
if found.start() != byte {
return None;
}
let chars = line[found.range()].chars().count();
Some((rc(cursor), (cursor.row(), cursor.column().get() + chars)))
}
fn forward_by(&self, from: Position, chars: usize) -> Position {
let text = self.inner.text();
let mut at = from;
for _ in 0..chars {
let next = motion::next_cluster(text, at);
if next.byte() == at.byte() {
break;
}
at = next;
}
at
}
pub fn span_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<Span> {
let text = self.inner.text();
let from = text.position(start.0, Column::new(start.1))?;
let to = text.position(end.0, Column::new(end.1))?;
text.span(from, to)
}
}
fn rc(position: Position) -> (usize, usize) {
(position.row(), position.column().get())
}
#[cfg(test)]
mod search_tests {
use super::*;
use crate::ropetext::Text;
fn buffer(text: &str, pattern: &str, cursor: (usize, usize)) -> RopeBuffer {
let mut buf = RopeBuffer::new(Text::from(text));
buf.set_search_pattern(pattern).expect("valid pattern");
buf.move_cursor(CursorMove::Jump(cursor.0, cursor.1));
buf
}
#[test]
fn a_forward_search_wraps_to_a_match_behind_the_cursor() {
let mut buf = buffer("xx foo", "foo", (0, 5));
assert!(buf.search_forward(false), "the match is behind the cursor");
assert_eq!(buf.cursor(), (0, 3));
}
#[test]
fn a_backward_search_wraps_to_a_match_ahead_of_the_cursor() {
let mut buf = buffer("xx foo", "foo", (0, 1));
assert!(buf.search_back(false));
assert_eq!(buf.cursor(), (0, 3));
}
#[test]
fn wrapping_crosses_rows_back_to_the_cursors_own_row() {
let mut buf = buffer("aaa\nxx foo", "foo", (1, 5));
assert!(buf.search_forward(false));
assert_eq!(buf.cursor(), (1, 3));
}
#[test]
fn the_only_match_is_re_offered_rather_than_reported_missing() {
let mut buf = buffer("xx foo", "foo", (0, 3));
assert!(buf.search_forward(false), "the one match is still a match");
assert_eq!(
buf.cursor(),
(0, 3),
"and the cursor has nowhere else to go"
);
}
#[test]
fn a_match_starting_inside_a_cluster_is_skipped_not_fatal() {
let mut buf = buffer(
"\u{1F469}\u{200D}\u{1F4BB}\nx\u{1F4BB}",
"\u{1F4BB}",
(0, 0),
);
assert!(buf.search_forward(false), "the row 1 match is reachable");
assert_eq!(buf.cursor(), (1, 1));
}
}
#[cfg(test)]
mod cluster_tests {
use super::*;
use crate::ropetext::Text;
#[test]
fn delete_str_spends_its_count_on_clusters() {
let mut buf = RopeBuffer::new(Text::from("[[\u{1F1EA}\u{1F1F8} rest"));
buf.move_cursor(CursorMove::Jump(0, 0));
buf.delete_str(3);
assert_eq!(buf.rows(), &[" rest"]);
}
#[test]
fn inserting_before_a_combining_mark_keeps_the_cursor_addressable() {
let mut buf = RopeBuffer::new(Text::from("\u{301}f"));
buf.move_cursor(CursorMove::Jump(0, 0));
buf.insert_char('a');
assert_eq!(buf.rows(), &["a\u{301}f"]);
}
}
#[cfg(test)]
mod damage_tests {
use super::*;
use crate::ropetext::Text;
#[test]
fn damage_from_several_edits_is_in_one_numbering() {
let mut buf = RopeBuffer::new(Text::from("r0\nr1\nr2\nr3\nr4"));
buf.edit(|b| {
b.move_cursor(CursorMove::Jump(4, 0));
b.insert_str("X");
b.move_cursor(CursorMove::Jump(0, 0));
b.insert_newline();
});
assert_eq!(buf.rows(), ["", "r0", "r1", "r2", "r3", "Xr4"]);
let damage = buf.take_outcome().damage.expect("the edits were reported");
assert!(
damage.contains(&5),
"the row edited first is row 5 once the group ends, but the damage \
reported was {damage:?} — a range in the older numbering"
);
}
}