use std::borrow::Cow;
use hjkl_buffer::Buffer as RopeBuffer;
use hjkl_buffer::Position;
use regex::Regex;
use crate::types::sealed::Sealed;
use crate::types::{Buffer, BufferEdit, Cursor, FoldOp, FoldProvider, Pos, Query, Search};
#[inline]
pub(crate) fn pos_to_position(p: Pos) -> Position {
Position {
row: p.line as usize,
col: p.col as usize,
}
}
#[inline]
pub(crate) fn position_to_pos(p: Position) -> Pos {
Pos {
line: p.row as u32,
col: p.col as u32,
}
}
impl Sealed for RopeBuffer {}
impl Cursor for RopeBuffer {
fn cursor(&self) -> Pos {
position_to_pos(RopeBuffer::cursor(self))
}
fn set_cursor(&mut self, pos: Pos) {
RopeBuffer::set_cursor(self, pos_to_position(pos));
}
fn byte_offset(&self, pos: Pos) -> usize {
let p = pos_to_position(pos);
let mut byte = 0usize;
for r in 0..p.row.min(self.row_count()) {
byte += self.line(r).map(str::len).unwrap_or(0) + 1; }
if let Some(line) = self.line(p.row) {
byte += p.byte_offset(line);
}
byte
}
fn pos_at_byte(&self, byte: usize) -> Pos {
let mut remaining = byte;
for r in 0..self.row_count() {
let line = self.line(r).unwrap_or("");
let line_bytes = line.len();
if remaining <= line_bytes {
let col = line[..remaining.min(line_bytes)].chars().count();
return Pos {
line: r as u32,
col: col as u32,
};
}
remaining -= line_bytes + 1;
}
let last = self.row_count().saturating_sub(1);
let line = self.line(last).unwrap_or("");
Pos {
line: last as u32,
col: line.chars().count() as u32,
}
}
}
impl Query for RopeBuffer {
fn line_count(&self) -> u32 {
self.row_count() as u32
}
fn line(&self, idx: u32) -> &str {
match RopeBuffer::line(self, idx as usize) {
Some(s) => s,
None => panic!(
"Query::line: index {idx} out of bounds (line_count = {})",
self.row_count()
),
}
}
fn len_bytes(&self) -> usize {
let n = self.row_count();
let mut total = 0usize;
for r in 0..n {
total += self.line(r).map(str::len).unwrap_or(0);
}
total + n.saturating_sub(1)
}
fn dirty_gen(&self) -> u64 {
RopeBuffer::dirty_gen(self)
}
fn slice(&self, range: core::ops::Range<Pos>) -> Cow<'_, str> {
let start = pos_to_position(range.start);
let end = pos_to_position(range.end);
if start >= end {
return Cow::Borrowed("");
}
if start.row == end.row {
if let Some(line) = RopeBuffer::line(self, start.row) {
let lo = start.byte_offset(line).min(line.len());
let hi = end.byte_offset(line).min(line.len());
return Cow::Borrowed(&line[lo..hi]);
}
return Cow::Borrowed("");
}
let mut out = String::new();
for r in start.row..=end.row.min(self.row_count().saturating_sub(1)) {
let line = RopeBuffer::line(self, r).unwrap_or("");
if r == start.row {
let lo = start.byte_offset(line).min(line.len());
out.push_str(&line[lo..]);
out.push('\n');
} else if r == end.row {
let hi = end.byte_offset(line).min(line.len());
out.push_str(&line[..hi]);
} else {
out.push_str(line);
out.push('\n');
}
}
Cow::Owned(out)
}
}
impl BufferEdit for RopeBuffer {
fn insert_at(&mut self, pos: Pos, text: &str) {
let at = clamp_to_buf(self, pos_to_position(pos));
let _ = self.apply_edit(hjkl_buffer::Edit::InsertStr {
at,
text: text.to_string(),
});
}
fn delete_range(&mut self, range: core::ops::Range<Pos>) {
let start = clamp_to_buf(self, pos_to_position(range.start));
let end = clamp_to_buf(self, pos_to_position(range.end));
if start >= end {
return;
}
let _ = self.apply_edit(hjkl_buffer::Edit::DeleteRange {
start,
end,
kind: hjkl_buffer::MotionKind::Char,
});
}
fn replace_range(&mut self, range: core::ops::Range<Pos>, replacement: &str) {
let start = clamp_to_buf(self, pos_to_position(range.start));
let end = clamp_to_buf(self, pos_to_position(range.end));
if start >= end {
let _ = self.apply_edit(hjkl_buffer::Edit::InsertStr {
at: start,
text: replacement.to_string(),
});
return;
}
let _ = self.apply_edit(hjkl_buffer::Edit::Replace {
start,
end,
with: replacement.to_string(),
});
}
fn replace_all(&mut self, text: &str) {
RopeBuffer::replace_all(self, text);
}
}
#[inline]
fn clamp_to_buf(buf: &RopeBuffer, p: Position) -> Position {
buf.clamp_position(p)
}
impl Search for RopeBuffer {
fn find_next(&self, from: Pos, pat: &Regex) -> Option<core::ops::Range<Pos>> {
let start = pos_to_position(from);
let total = self.row_count();
if total == 0 {
return None;
}
let wrap = true;
let from_line = RopeBuffer::line(self, start.row).unwrap_or("");
let from_byte = start.byte_offset(from_line).min(from_line.len());
if let Some(m) = pat.find_at(from_line, from_byte) {
return Some(byte_range_to_pos_range(
start.row,
m.start(),
start.row,
m.end(),
from_line,
));
}
for offset in 1..total {
let row = start.row + offset;
if row >= total && !wrap {
break;
}
let row = row % total;
if !wrap && row <= start.row {
break;
}
let line = RopeBuffer::line(self, row).unwrap_or("");
if let Some(m) = pat.find(line) {
return Some(byte_range_to_pos_range(row, m.start(), row, m.end(), line));
}
if row == start.row {
break;
}
}
None
}
fn find_prev(&self, from: Pos, pat: &Regex) -> Option<core::ops::Range<Pos>> {
let start = pos_to_position(from);
let total = self.row_count();
if total == 0 {
return None;
}
let wrap = true;
let from_line = RopeBuffer::line(self, start.row).unwrap_or("");
let from_byte = start.byte_offset(from_line).min(from_line.len());
let mut best: Option<(usize, usize)> = None;
for m in pat.find_iter(from_line) {
if m.start() <= from_byte {
best = Some((m.start(), m.end()));
} else {
break;
}
}
if let Some((s, e)) = best {
return Some(byte_range_to_pos_range(
start.row, s, start.row, e, from_line,
));
}
for offset in 1..total {
let row = if offset > start.row {
if !wrap {
break;
}
total - (offset - start.row)
} else {
start.row - offset
};
if !wrap && row >= start.row {
break;
}
let line = RopeBuffer::line(self, row).unwrap_or("");
let last = pat.find_iter(line).last();
if let Some(m) = last {
return Some(byte_range_to_pos_range(row, m.start(), row, m.end(), line));
}
if row == start.row {
break;
}
}
None
}
}
#[inline]
fn byte_range_to_pos_range(
s_row: usize,
s_byte: usize,
e_row: usize,
e_byte: usize,
line: &str,
) -> core::ops::Range<Pos> {
let s_col = line[..s_byte.min(line.len())].chars().count();
let e_col = line[..e_byte.min(line.len())].chars().count();
Pos {
line: s_row as u32,
col: s_col as u32,
}..Pos {
line: e_row as u32,
col: e_col as u32,
}
}
impl Buffer for RopeBuffer {}
pub struct BufferFoldProvider<'a> {
buffer: &'a RopeBuffer,
}
impl<'a> BufferFoldProvider<'a> {
pub fn new(buffer: &'a RopeBuffer) -> Self {
Self { buffer }
}
}
impl FoldProvider for BufferFoldProvider<'_> {
fn next_visible_row(&self, row: usize, _row_count: usize) -> Option<usize> {
RopeBuffer::next_visible_row(self.buffer, row)
}
fn prev_visible_row(&self, row: usize) -> Option<usize> {
RopeBuffer::prev_visible_row(self.buffer, row)
}
fn is_row_hidden(&self, row: usize) -> bool {
RopeBuffer::is_row_hidden(self.buffer, row)
}
fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)> {
let f = self.buffer.fold_at_row(row)?;
Some((f.start_row, f.end_row, f.closed))
}
}
pub struct BufferFoldProviderMut<'a> {
buffer: &'a mut RopeBuffer,
}
impl<'a> BufferFoldProviderMut<'a> {
pub fn new(buffer: &'a mut RopeBuffer) -> Self {
Self { buffer }
}
}
impl FoldProvider for BufferFoldProviderMut<'_> {
fn next_visible_row(&self, row: usize, _row_count: usize) -> Option<usize> {
RopeBuffer::next_visible_row(self.buffer, row)
}
fn prev_visible_row(&self, row: usize) -> Option<usize> {
RopeBuffer::prev_visible_row(self.buffer, row)
}
fn is_row_hidden(&self, row: usize) -> bool {
RopeBuffer::is_row_hidden(self.buffer, row)
}
fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)> {
let f = self.buffer.fold_at_row(row)?;
Some((f.start_row, f.end_row, f.closed))
}
fn apply(&mut self, op: FoldOp) {
match op {
FoldOp::Add {
start_row,
end_row,
closed,
} => {
self.buffer.add_fold(start_row, end_row, closed);
}
FoldOp::RemoveAt(row) => {
self.buffer.remove_fold_at(row);
}
FoldOp::OpenAt(row) => {
self.buffer.open_fold_at(row);
}
FoldOp::CloseAt(row) => {
self.buffer.close_fold_at(row);
}
FoldOp::ToggleAt(row) => {
self.buffer.toggle_fold_at(row);
}
FoldOp::OpenAll => {
self.buffer.open_all_folds();
}
FoldOp::CloseAll => {
self.buffer.close_all_folds();
}
FoldOp::ClearAll => {
self.buffer.clear_all_folds();
}
FoldOp::Invalidate { start_row, end_row } => {
self.buffer.invalidate_folds_in_range(start_row, end_row);
}
}
}
fn invalidate_range(&mut self, start_row: usize, end_row: usize) {
self.buffer.invalidate_folds_in_range(start_row, end_row);
}
}
pub struct SnapshotFoldProvider {
folds: Vec<hjkl_buffer::Fold>,
row_count: usize,
}
impl SnapshotFoldProvider {
pub fn from_buffer(buffer: &RopeBuffer) -> Self {
Self {
folds: buffer.folds().to_vec(),
row_count: buffer.row_count(),
}
}
fn snapshot_is_row_hidden(&self, row: usize) -> bool {
self.folds.iter().any(|f| f.hides(row))
}
}
impl FoldProvider for SnapshotFoldProvider {
fn next_visible_row(&self, row: usize, _row_count: usize) -> Option<usize> {
let last = self.row_count.saturating_sub(1);
if last == 0 && row == 0 {
return None;
}
let mut r = row.checked_add(1)?;
while r <= last && self.snapshot_is_row_hidden(r) {
r += 1;
}
(r <= last).then_some(r)
}
fn prev_visible_row(&self, row: usize) -> Option<usize> {
let mut r = row.checked_sub(1)?;
while self.snapshot_is_row_hidden(r) {
r = r.checked_sub(1)?;
}
Some(r)
}
fn is_row_hidden(&self, row: usize) -> bool {
self.snapshot_is_row_hidden(row)
}
fn fold_at_row(&self, row: usize) -> Option<(usize, usize, bool)> {
self.folds
.iter()
.find(|f| f.contains(row))
.map(|f| (f.start_row, f.end_row, f.closed))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rope_buffer_implements_spec_buffer() {
fn assert_buffer<B: Buffer>() {}
fn assert_cursor<B: Cursor>() {}
fn assert_query<B: Query>() {}
fn assert_edit<B: BufferEdit>() {}
fn assert_search<B: Search>() {}
assert_buffer::<RopeBuffer>();
assert_cursor::<RopeBuffer>();
assert_query::<RopeBuffer>();
assert_edit::<RopeBuffer>();
assert_search::<RopeBuffer>();
}
#[test]
fn cursor_roundtrip() {
let mut b = RopeBuffer::from_str("hello\nworld");
Cursor::set_cursor(&mut b, Pos::new(1, 3));
assert_eq!(Cursor::cursor(&b), Pos::new(1, 3));
}
#[test]
fn query_line_count_and_line() {
let b = RopeBuffer::from_str("a\nb\nc");
assert_eq!(Query::line_count(&b), 3);
assert_eq!(Query::line(&b, 0), "a");
assert_eq!(Query::line(&b, 2), "c");
}
#[test]
fn query_len_bytes_matches_join() {
let b = RopeBuffer::from_str("foo\nbar\nbaz");
assert_eq!(Query::len_bytes(&b), b.as_string().len());
}
#[test]
fn query_slice_single_line_borrows() {
let b = RopeBuffer::from_str("hello world");
let s = Query::slice(&b, Pos::new(0, 0)..Pos::new(0, 5));
assert_eq!(&*s, "hello");
assert!(matches!(s, Cow::Borrowed(_)));
}
#[test]
fn query_slice_multiline_allocates() {
let b = RopeBuffer::from_str("ab\ncd\nef");
let s = Query::slice(&b, Pos::new(0, 1)..Pos::new(2, 1));
assert_eq!(&*s, "b\ncd\ne");
assert!(matches!(s, Cow::Owned(_)));
}
#[test]
fn cursor_byte_offset_and_inverse() {
let b = RopeBuffer::from_str("hello\nworld");
let p = Pos::new(1, 0);
assert_eq!(Cursor::byte_offset(&b, p), 6);
assert_eq!(Cursor::pos_at_byte(&b, 6), p);
let p2 = Pos::new(1, 3);
let off = Cursor::byte_offset(&b, p2);
assert_eq!(Cursor::pos_at_byte(&b, off), p2);
}
#[test]
fn buffer_edit_insert_delete_replace() {
let mut b = RopeBuffer::from_str("hello");
BufferEdit::insert_at(&mut b, Pos::new(0, 5), " world");
assert_eq!(b.as_string(), "hello world");
BufferEdit::delete_range(&mut b, Pos::new(0, 5)..Pos::new(0, 11));
assert_eq!(b.as_string(), "hello");
BufferEdit::replace_range(&mut b, Pos::new(0, 0)..Pos::new(0, 5), "HI");
assert_eq!(b.as_string(), "HI");
}
#[test]
fn buffer_edit_default_replace_all_routes_through_replace_range() {
struct MockBuf {
cursor: Pos,
lines: Vec<String>,
last_replace_range: Option<core::ops::Range<Pos>>,
}
impl Sealed for MockBuf {}
impl Cursor for MockBuf {
fn cursor(&self) -> Pos {
self.cursor
}
fn set_cursor(&mut self, p: Pos) {
self.cursor = p;
}
fn byte_offset(&self, _p: Pos) -> usize {
0
}
fn pos_at_byte(&self, _b: usize) -> Pos {
Pos::ORIGIN
}
}
impl Query for MockBuf {
fn line_count(&self) -> u32 {
self.lines.len() as u32
}
fn line(&self, idx: u32) -> &str {
&self.lines[idx as usize]
}
fn len_bytes(&self) -> usize {
0
}
fn slice(&self, _r: core::ops::Range<Pos>) -> Cow<'_, str> {
Cow::Borrowed("")
}
}
impl BufferEdit for MockBuf {
fn insert_at(&mut self, _p: Pos, _t: &str) {}
fn delete_range(&mut self, _r: core::ops::Range<Pos>) {}
fn replace_range(&mut self, range: core::ops::Range<Pos>, _t: &str) {
self.last_replace_range = Some(range);
}
}
impl Search for MockBuf {
fn find_next(&self, _f: Pos, _p: &Regex) -> Option<core::ops::Range<Pos>> {
None
}
fn find_prev(&self, _f: Pos, _p: &Regex) -> Option<core::ops::Range<Pos>> {
None
}
}
impl Buffer for MockBuf {}
let mut m = MockBuf {
cursor: Pos::ORIGIN,
lines: vec!["hi".into()],
last_replace_range: None,
};
BufferEdit::replace_all(&mut m, "new content");
let r = m
.last_replace_range
.expect("default impl must hit replace_range");
assert_eq!(r.start, Pos::ORIGIN);
assert_eq!(r.end.line, u32::MAX);
assert_eq!(r.end.col, u32::MAX);
}
#[test]
fn buffer_edit_replace_all_rebuilds_content() {
let mut b = RopeBuffer::from_str("hello\nworld");
Cursor::set_cursor(&mut b, Pos::new(1, 3));
BufferEdit::replace_all(&mut b, "alpha\nbeta\ngamma");
assert_eq!(b.as_string(), "alpha\nbeta\ngamma");
assert_eq!(Query::line_count(&b), 3);
let c = Cursor::cursor(&b);
assert!((c.line as usize) < Query::line_count(&b) as usize);
}
#[test]
fn search_find_next_same_row() {
let b = RopeBuffer::from_str("abc def abc");
let pat = Regex::new("abc").unwrap();
let r = Search::find_next(&b, Pos::new(0, 0), &pat).unwrap();
assert_eq!(r, Pos::new(0, 0)..Pos::new(0, 3));
let r2 = Search::find_next(&b, Pos::new(0, 1), &pat).unwrap();
assert_eq!(r2, Pos::new(0, 8)..Pos::new(0, 11));
}
#[test]
fn search_find_next_wraps() {
let b = RopeBuffer::from_str("foo\nbar\nfoo");
let pat = Regex::new("foo").unwrap();
let r = Search::find_next(&b, Pos::new(1, 0), &pat).unwrap();
assert_eq!(r, Pos::new(2, 0)..Pos::new(2, 3));
}
#[test]
fn search_find_prev_same_row() {
let b = RopeBuffer::from_str("abc def abc");
let pat = Regex::new("abc").unwrap();
let r = Search::find_prev(&b, Pos::new(0, 11), &pat).unwrap();
assert_eq!(r, Pos::new(0, 8)..Pos::new(0, 11));
}
#[test]
fn pos_position_roundtrip() {
let p = Pos::new(7, 3);
assert_eq!(position_to_pos(pos_to_position(p)), p);
}
#[test]
fn fold_provider_mut_apply_add_open_close_toggle() {
let mut buf = RopeBuffer::from_str("a\nb\nc\nd\ne");
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.apply(FoldOp::Add {
start_row: 1,
end_row: 3,
closed: true,
});
assert_eq!(p.fold_at_row(2), Some((1, 3, true)));
p.apply(FoldOp::OpenAt(2));
assert_eq!(p.fold_at_row(2), Some((1, 3, false)));
p.apply(FoldOp::CloseAt(2));
assert_eq!(p.fold_at_row(2), Some((1, 3, true)));
p.apply(FoldOp::ToggleAt(2));
assert_eq!(p.fold_at_row(2), Some((1, 3, false)));
}
assert_eq!(buf.folds().len(), 1);
}
#[test]
fn fold_provider_mut_apply_open_close_clear_all() {
let mut buf = RopeBuffer::from_str("a\nb\nc\nd\ne");
buf.add_fold(0, 1, false);
buf.add_fold(2, 3, true);
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.apply(FoldOp::CloseAll);
}
assert!(buf.folds().iter().all(|f| f.closed));
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.apply(FoldOp::OpenAll);
}
assert!(buf.folds().iter().all(|f| !f.closed));
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.apply(FoldOp::ClearAll);
}
assert!(buf.folds().is_empty());
}
#[test]
fn fold_provider_mut_invalidate_range_drops_overlapping() {
let mut buf = RopeBuffer::from_str("a\nb\nc\nd\ne");
buf.add_fold(0, 1, true);
buf.add_fold(2, 3, true);
buf.add_fold(4, 4, true);
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.invalidate_range(2, 3);
}
let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
assert_eq!(starts, vec![0, 4]);
}
#[test]
fn fold_provider_mut_apply_remove_at() {
let mut buf = RopeBuffer::from_str("a\nb\nc\nd\ne");
buf.add_fold(1, 3, true);
{
let mut p = BufferFoldProviderMut::new(&mut buf);
p.apply(FoldOp::RemoveAt(2));
}
assert!(buf.folds().is_empty());
}
#[test]
fn noop_fold_provider_apply_is_noop() {
let mut p = crate::types::NoopFoldProvider;
FoldProvider::apply(&mut p, FoldOp::OpenAll);
FoldProvider::invalidate_range(&mut p, 0, 5);
assert!(!FoldProvider::is_row_hidden(&p, 3));
}
}