use crate::buf::opt::BufferOptions;
use crate::buf::unicode;
use crate::prelude::*;
pub use cidx::ColumnIndex;
use compact_str::{CompactString, ToCompactString};
use lru::LruCache;
use ropey::{Rope, RopeSlice};
use std::cell::RefCell;
pub mod cidx;
#[cfg(test)]
mod cidx_tests;
#[derive(Debug)]
pub struct Text {
rope: Rope,
cached_lines_width: RefCell<LruCache<usize, ColumnIndex, RandomState>>,
options: BufferOptions,
}
arc_mutex_ptr!(Text);
#[inline]
fn _cached_size(canvas_size: U16Size) -> std::num::NonZeroUsize {
std::num::NonZeroUsize::new(canvas_size.height() as usize * 3 + 3).unwrap()
}
impl Text {
pub fn new(opts: BufferOptions, canvas_size: U16Size, rope: Rope) -> Self {
let cache_size = _cached_size(canvas_size);
Self {
rope,
cached_lines_width: RefCell::new(LruCache::with_hasher(
cache_size,
RandomState::default(),
)),
options: opts,
}
}
}
impl Text {
pub fn char_width(&self, c: char) -> usize {
unicode::char_width(&self.options, c)
}
pub fn char_symbol(&self, c: char) -> CompactString {
unicode::char_symbol(&self.options, c)
}
pub fn char_symbol_and_width(&self, c: char) -> (CompactString, usize) {
(
unicode::char_symbol(&self.options, c),
unicode::char_width(&self.options, c),
)
}
}
impl Text {
pub fn rope(&self) -> &Rope {
&self.rope
}
fn rope_mut(&mut self) -> &mut Rope {
&mut self.rope
}
pub fn clone_line(
&self,
line_idx: usize,
start_char_idx: usize,
max_chars: usize,
) -> Option<String> {
match self.rope.get_line(line_idx) {
Some(bufline) => match bufline.get_chars_at(start_char_idx) {
Some(chars_iter) => {
let mut builder = String::with_capacity(max_chars);
for (i, c) in chars_iter.enumerate() {
if i >= max_chars {
return Some(builder);
}
builder.push(c);
}
Some(builder)
}
None => None,
},
None => None,
}
}
fn _is_eol_on_line(&self, line: &RopeSlice, char_idx: usize) -> bool {
use crate::defaults::ascii::end_of_line as ascii_eol;
let len_chars = line.len_chars();
let is_crlf = len_chars >= 2
&& char_idx >= len_chars - 2
&& char_idx < len_chars
&& format!("{}{}", line.char(len_chars - 2), line.char(len_chars - 1))
== ascii_eol::CRLF;
let is_cr_or_lf = len_chars >= 1
&& char_idx == len_chars - 1
&& (format!("{}", line.char(len_chars - 1)) == ascii_eol::CR
|| format!("{}", line.char(len_chars - 1)) == ascii_eol::LF);
is_crlf || is_cr_or_lf
}
fn _is_eol_on_whole_text(&self, char_idx: usize) -> bool {
use crate::defaults::ascii::end_of_line as ascii_eol;
let r = &self.rope;
let len_chars = r.len_chars();
let is_crlf = len_chars >= 2
&& char_idx >= len_chars - 2
&& char_idx < len_chars
&& format!("{}{}", r.char(len_chars - 2), r.char(len_chars - 1))
== ascii_eol::CRLF;
let is_cr_or_lf = len_chars >= 1
&& char_idx == len_chars - 1
&& (format!("{}", r.char(len_chars - 1)) == ascii_eol::CR
|| format!("{}", r.char(len_chars - 1)) == ascii_eol::LF);
is_crlf || is_cr_or_lf
}
pub fn last_char_on_line(&self, line_idx: usize) -> Option<usize> {
match self.rope.get_line(line_idx) {
Some(line) => {
let line_len_chars = line.len_chars();
if line_len_chars > 0 {
Some(line_len_chars - 1)
} else {
None
}
}
None => None,
}
}
pub fn last_char_on_line_no_eol(&self, line_idx: usize) -> Option<usize> {
match self.rope.get_line(line_idx) {
Some(line) => match self.last_char_on_line(line_idx) {
Some(last_char) => {
let mut c = last_char;
while c > 0 && self._is_eol_on_line(&line, c) {
c = c.saturating_sub(1);
}
if self._is_eol_on_line(&line, c) {
None
} else {
Some(c)
}
}
None => None,
},
None => None,
}
}
pub fn is_eol(&self, line_idx: usize, char_idx: usize) -> bool {
match self.rope.get_line(line_idx) {
Some(line) => self._is_eol_on_line(&line, char_idx),
None => false,
}
}
}
impl Text {
pub fn options(&self) -> &BufferOptions {
&self.options
}
pub fn set_options(&mut self, options: &BufferOptions) {
self.options = *options;
}
}
impl Text {
pub fn width_before(&self, line_idx: usize, char_idx: usize) -> usize {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.width_before(&self.options, &rope_line, char_idx)
}
pub fn width_until(&self, line_idx: usize, char_idx: usize) -> usize {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.width_until(&self.options, &rope_line, char_idx)
}
pub fn char_before(&self, line_idx: usize, width: usize) -> Option<usize> {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.char_before(&self.options, &rope_line, width)
}
pub fn char_at(&self, line_idx: usize, width: usize) -> Option<usize> {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.char_at(&self.options, &rope_line, width)
}
pub fn char_after(&self, line_idx: usize, width: usize) -> Option<usize> {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.char_after(&self.options, &rope_line, width)
}
pub fn last_char_until(
&self,
line_idx: usize,
width: usize,
) -> Option<usize> {
let rope_line = self.rope.line(line_idx);
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
ColumnIndex::with_capacity(rope_line.len_chars())
})
.last_char_until(&self.options, &rope_line, width)
}
fn truncate_cached_line_since_char(&self, line_idx: usize, char_idx: usize) {
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
let rope_line = self.rope.line(line_idx);
ColumnIndex::with_capacity(rope_line.len_chars())
})
.truncate_since_char(char_idx)
}
#[allow(dead_code)]
fn truncate_cached_line_since_width(&self, line_idx: usize, width: usize) {
self
.cached_lines_width
.borrow_mut()
.get_or_insert_mut(line_idx, || -> ColumnIndex {
let rope_line = self.rope.line(line_idx);
ColumnIndex::with_capacity(rope_line.len_chars())
})
.truncate_since_width(width)
}
#[allow(dead_code)]
fn remove_cached_line(&self, line_idx: usize) {
self.cached_lines_width.borrow_mut().pop(&line_idx);
}
fn retain_cached_lines<F>(&self, f: F)
where
F: Fn(
/* line_idx */ &usize,
/* column_idx */ &ColumnIndex,
) -> bool,
{
let mut cached_width = self.cached_lines_width.borrow_mut();
let to_be_removed_lines: Vec<usize> = cached_width
.iter()
.filter(|(line_idx, column_idx)| !f(line_idx, column_idx))
.map(|(line_idx, _)| *line_idx)
.collect();
for line_idx in to_be_removed_lines.iter() {
cached_width.pop(line_idx);
}
}
fn clear_cached_lines(&self) {
self.cached_lines_width.borrow_mut().clear()
}
#[allow(dead_code)]
fn resize_cached_lines(&self, canvas_size: U16Size) {
let new_cache_size = _cached_size(canvas_size);
let mut cached_width = self.cached_lines_width.borrow_mut();
if new_cache_size > cached_width.cap() {
cached_width.resize(new_cache_size);
}
}
}
#[cfg(test)]
fn _ropeline_to_string(bufline: &ropey::RopeSlice) -> String {
let mut builder = String::with_capacity(bufline.len_chars());
for c in bufline.chars() {
builder.push(c);
}
builder
}
impl Text {
#[cfg(not(test))]
fn dbg_print_textline_absolutely(
&mut self,
_line_idx: usize,
_absolute_char_idx: usize,
_msg: &str,
) {
}
#[cfg(test)]
fn dbg_print_textline_absolutely(
&mut self,
line_idx: usize,
absolute_char_idx: usize,
msg: &str,
) {
trace!(
"{} text line:{},absolute_char:{}",
msg, line_idx, absolute_char_idx
);
match self.rope().get_line(line_idx) {
Some(line) => {
trace!("len_chars:{}", line.len_chars());
let start_char_on_line = self.rope().line_to_char(line_idx);
let mut builder1 = String::new();
let mut builder2 = String::new();
for (i, c) in line.chars().enumerate() {
let w = self.char_width(c);
if w > 0 {
builder1.push(c);
}
let s: String = std::iter::repeat_n(
if i + start_char_on_line == absolute_char_idx {
'^'
} else {
' '
},
w,
)
.collect();
builder2.push_str(s.as_str());
}
trace!("-{}-", builder1);
trace!("-{}-", builder2);
}
None => trace!("line not exist"),
}
trace!("{} whole text:", msg);
for i in 0..self.rope().len_lines() {
trace!("{i}:{:?}", _ropeline_to_string(&self.rope().line(i)));
}
}
#[cfg(not(test))]
fn dbg_print_textline(
&mut self,
_line_idx: usize,
_char_idx: usize,
_msg: &str,
) {
}
#[cfg(test)]
fn dbg_print_textline(
&mut self,
line_idx: usize,
char_idx: usize,
msg: &str,
) {
trace!("{} text line:{},char:{}", msg, line_idx, char_idx);
match self.rope().get_line(line_idx) {
Some(bufline) => {
trace!("len_chars:{}", bufline.len_chars());
let mut builder1 = String::new();
let mut builder2 = String::new();
for (i, c) in bufline.chars().enumerate() {
let w = self.char_width(c);
if w > 0 {
builder1.push(c);
}
let s: String =
std::iter::repeat_n(if i == char_idx { '^' } else { ' ' }, w)
.collect();
builder2.push_str(s.as_str());
}
trace!("-{}-", builder1);
trace!("-{}-", builder2);
}
None => trace!("line not exist"),
}
trace!("{}, whole buffer:", msg);
for i in 0..self.rope().len_lines() {
trace!("{i}:{:?}", _ropeline_to_string(&self.rope().line(i)));
}
}
}
impl Text {
fn append_eol_at_end_if_not_exist(&mut self) {
let eol = self.options().end_of_line();
let buffer_len_chars = self.rope.len_chars();
let last_char_on_buf = buffer_len_chars.saturating_sub(1);
match self.rope.get_char(last_char_on_buf) {
Some(_c) => {
let c_is_eol = self._is_eol_on_whole_text(last_char_on_buf);
if !c_is_eol {
self
.rope_mut()
.insert(buffer_len_chars, eol.to_compact_string().as_str());
let inserted_line_idx = self.rope.char_to_line(buffer_len_chars);
self.retain_cached_lines(|line_idx, _column_idx| {
*line_idx < inserted_line_idx
});
self.dbg_print_textline_absolutely(
inserted_line_idx,
buffer_len_chars,
"Eol appended(non-empty)",
);
}
}
None => {
self
.rope_mut()
.insert(0_usize, eol.to_compact_string().as_str());
self.clear_cached_lines();
self.dbg_print_textline_absolutely(
0_usize,
buffer_len_chars,
"Eol appended(empty)",
);
}
}
}
pub fn insert_at(
&mut self,
line_idx: usize,
char_idx: usize,
payload: CompactString,
) -> (usize, usize) {
debug_assert!(self.rope.get_line(line_idx).is_some());
debug_assert!(char_idx <= self.rope.line(line_idx).len_chars());
let absolute_line_idx = self.rope.line_to_char(line_idx);
let absolute_char_idx_before_insert = absolute_line_idx + char_idx;
self.dbg_print_textline(line_idx, char_idx, "Before insert");
self
.rope_mut()
.insert(absolute_char_idx_before_insert, payload.as_str());
let absolute_char_idx_after_inserted =
absolute_char_idx_before_insert + payload.chars().count();
let line_idx_after_inserted =
self.rope.char_to_line(absolute_char_idx_after_inserted);
let absolute_line_idx_after_inserted =
self.rope.line_to_char(line_idx_after_inserted);
let char_idx_after_inserted =
absolute_char_idx_after_inserted - absolute_line_idx_after_inserted;
if line_idx == line_idx_after_inserted {
debug_assert!(char_idx_after_inserted >= char_idx);
let min_cursor_char_idx =
std::cmp::min(char_idx_after_inserted, char_idx);
self.truncate_cached_line_since_char(
line_idx,
min_cursor_char_idx.saturating_sub(1),
);
} else {
let min_cursor_line_idx =
std::cmp::min(line_idx_after_inserted, line_idx);
self.retain_cached_lines(|line_idx, _column_idx| {
*line_idx < min_cursor_line_idx
});
}
self.append_eol_at_end_if_not_exist();
self.dbg_print_textline(
line_idx_after_inserted,
char_idx_after_inserted,
"After inserted",
);
(line_idx_after_inserted, char_idx_after_inserted)
}
fn _n_chars_to_left(&self, absolute_char_idx: usize, n: usize) -> usize {
use crate::defaults::ascii::end_of_line as ascii_eol;
debug_assert!(n > 0);
let mut i = absolute_char_idx as isize;
let mut acc = 0;
while acc < n && i >= 0 {
let c1 = self.rope.get_char(i as usize);
let c2 = if i > 0 {
self.rope.get_char((i - 1) as usize)
} else {
None
};
if c1.is_some()
&& c2.is_some()
&& format!("{}{}", c2.unwrap(), c1.unwrap()) == ascii_eol::CRLF
{
i -= 2;
} else {
i -= 1;
}
acc += 1;
}
std::cmp::max(i, 0) as usize
}
fn _n_chars_to_right(&self, absolute_char_idx: usize, n: usize) -> usize {
debug_assert!(n > 0);
let len_chars = self.rope.len_chars();
let mut i = absolute_char_idx;
let mut acc = 0;
while acc < n && i <= len_chars {
let c1 = self.rope.get_char(i);
let c2 = self.rope.get_char(i + 1);
if c1.is_some()
&& c2.is_some()
&& format!("{}{}", c1.unwrap(), c2.unwrap())
== crate::defaults::ascii::end_of_line::CRLF
{
i += 2;
} else {
i += 1;
}
acc += 1;
}
i
}
pub fn delete_at(
&mut self,
line_idx: usize,
char_idx: usize,
n: isize,
) -> Option<(usize, usize)> {
debug_assert!(self.rope.get_line(line_idx).is_some());
debug_assert!(char_idx < self.rope.line(line_idx).len_chars());
let cursor_char_absolute_pos_before_delete =
self.rope.line_to_char(line_idx) + char_idx;
self.dbg_print_textline(line_idx, char_idx, "Before delete");
let to_be_deleted_range = if n > 0 {
let upper = self
._n_chars_to_right(cursor_char_absolute_pos_before_delete, n as usize);
debug_assert!(upper <= self.rope.len_chars());
cursor_char_absolute_pos_before_delete..upper
} else {
let lower = self._n_chars_to_left(
cursor_char_absolute_pos_before_delete,
(-n) as usize,
);
lower..cursor_char_absolute_pos_before_delete
};
if to_be_deleted_range.is_empty() {
return None;
}
self.rope_mut().remove(to_be_deleted_range.clone());
let cursor_char_absolute_pos_after_deleted = to_be_deleted_range.start;
let cursor_char_absolute_pos_after_deleted = std::cmp::min(
cursor_char_absolute_pos_after_deleted,
self.rope.len_chars(),
);
let cursor_line_idx_after_deleted = self
.rope
.char_to_line(cursor_char_absolute_pos_after_deleted);
let cursor_line_absolute_pos_after_deleted =
self.rope.line_to_char(cursor_line_idx_after_deleted);
let cursor_char_idx_after_deleted = cursor_char_absolute_pos_after_deleted
- cursor_line_absolute_pos_after_deleted;
if line_idx == cursor_line_idx_after_deleted {
let min_cursor_char_idx =
std::cmp::min(cursor_char_idx_after_deleted, char_idx);
self.truncate_cached_line_since_char(line_idx, min_cursor_char_idx);
} else {
let min_cursor_line_idx =
std::cmp::min(cursor_line_idx_after_deleted, line_idx);
self.retain_cached_lines(|line_idx, _column_idx| {
*line_idx < min_cursor_line_idx
});
}
self.append_eol_at_end_if_not_exist();
self.dbg_print_textline(
cursor_line_idx_after_deleted,
cursor_char_idx_after_deleted,
"After deleted",
);
Some((cursor_line_idx_after_deleted, cursor_char_idx_after_deleted))
}
pub fn clear(&mut self) {
self.rope_mut().remove(0..);
self.clear_cached_lines();
}
}