pub mod cache;
pub mod cidx;
#[cfg(test)]
mod cache_tests;
#[cfg(test)]
mod cidx_tests;
use crate::buf::opt::BufferOptions;
use crate::buf::opt::EndOfLineOption;
use crate::buf::unicode;
use crate::prelude::*;
use arcstr::ArcStr;
use cache::CachedLines;
use cache::CachedLinesKey;
use cache::CachedWidth;
pub use cidx::ColumnIndex;
use compact_str::CompactString;
use compact_str::ToCompactString;
use ropey::Rope;
use ropey::RopeSlice;
use std::cell::RefCell;
use std::ops::Range;
#[derive(Debug)]
pub struct Text {
rope: Rope,
options: BufferOptions,
cached_width: RefCell<CachedWidth>,
cached_lines: RefCell<CachedLines>,
}
arc_mutex_ptr!(Text);
impl Text {
pub fn new(opts: BufferOptions, canvas_size: U16Size, rope: Rope) -> Self {
Self {
rope,
options: opts,
cached_width: RefCell::new(CachedWidth::new(canvas_size)),
cached_lines: RefCell::new(CachedLines::new(canvas_size)),
}
}
}
#[cfg(test)]
impl Drop for Text {
fn drop(&mut self) {
let cached_width = self.cached_width.borrow();
if cached_width.stats().total() > 0 {
trace!("|drop| cached_width {}", cached_width.stats());
}
let cached_lines = self.cached_lines.borrow();
if cached_lines.stats().total() > 0 {
trace!("|drop| cached_lines {}", cached_lines.stats());
}
}
}
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
}
fn _clone_line_impl(
&self,
line_idx: usize,
start_char_idx: usize,
max_chars_width: usize,
) -> Option<ArcStr> {
match self.rope.get_line(line_idx) {
Some(buffer_line) => match buffer_line.get_chars_at(start_char_idx) {
Some(chars_iter) => {
let mut w: usize = 0;
let mut builder = String::with_capacity(max_chars_width);
for c in chars_iter {
w += unicode::char_width(self.options(), c);
if w >= max_chars_width {
return Some(ArcStr::from(builder));
}
builder.push(c);
}
Some(ArcStr::from(builder))
}
None => None,
},
None => None,
}
}
fn _clone_line_impl_wrap(
&self,
line_idx: usize,
start_char_idx: usize,
max_chars_width: usize,
skip_cache: bool,
) -> Option<ArcStr> {
let mut cached_lines = self.cached_lines.borrow_mut();
let key = CachedLinesKey {
line_idx,
start_char_idx,
max_chars: max_chars_width,
};
if skip_cache {
self._clone_line_impl(line_idx, start_char_idx, max_chars_width)
} else {
cached_lines
.get_or_insert(&key, || {
self._clone_line_impl(line_idx, start_char_idx, max_chars_width)
})
.cloned()
}
}
pub fn clone_line(
&self,
line_idx: usize,
start_char_idx: usize,
max_chars_width: usize,
) -> Option<ArcStr> {
let result1 = self._clone_line_impl_wrap(
line_idx,
start_char_idx,
max_chars_width,
false,
);
if cfg!(debug_assertions) {
let result2 = self._clone_line_impl_wrap(
line_idx,
start_char_idx,
max_chars_width,
true,
);
debug_assert_eq!(result1, result2);
}
result1
}
pub fn is_eol_on_rope_line(line: &RopeSlice, char_idx: usize) -> bool {
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))
== EndOfLineOption::Crlf.to_compact_string();
let is_cr_or_lf = len_chars >= 1
&& char_idx == len_chars - 1
&& (format!("{}", line.char(len_chars - 1))
== EndOfLineOption::Cr.to_compact_string()
|| format!("{}", line.char(len_chars - 1))
== EndOfLineOption::Lf.to_compact_string());
is_crlf || is_cr_or_lf
}
pub fn is_eol_on_rope(rope: &Rope, absolute_char_idx: usize) -> bool {
let len_chars = rope.len_chars();
let is_crlf = len_chars >= 2
&& absolute_char_idx >= len_chars - 2
&& absolute_char_idx < len_chars
&& format!("{}{}", rope.char(len_chars - 2), rope.char(len_chars - 1))
== EndOfLineOption::Crlf.to_compact_string();
let is_cr_or_lf = len_chars >= 1
&& absolute_char_idx == len_chars - 1
&& (format!("{}", rope.char(len_chars - 1))
== EndOfLineOption::Cr.to_compact_string()
|| format!("{}", rope.char(len_chars - 1))
== EndOfLineOption::Lf.to_compact_string());
is_crlf || is_cr_or_lf
}
pub fn last_char_idx_on_line_include_eol(
&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_idx_on_line_exclude_eol(
&self,
line_idx: usize,
) -> Option<usize> {
match self.rope.get_line(line_idx) {
Some(line) => match self.last_char_idx_on_line_include_eol(line_idx) {
Some(last_char) => {
let mut c = last_char;
while c > 0 && Self::is_eol_on_rope_line(&line, c) {
c = c.saturating_sub(1);
}
if Self::is_eol_on_rope_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_rope_line(&line, char_idx),
None => false,
}
}
pub fn is_eol_or_line_end(&self, line_idx: usize, char_idx: usize) -> bool {
match self.rope.get_line(line_idx) {
Some(line) => {
char_idx >= line.len_chars()
|| Self::is_eol_on_rope_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 {
fn with_cached_column_idx<U, F>(
&self,
line_idx: usize,
rope_line: &RopeSlice,
f: F,
) -> U
where
F: FnOnce(&mut ColumnIndex) -> U,
{
f(self
.cached_width
.borrow_mut()
.get_or_insert_mut(&line_idx, || {
Some(ColumnIndex::with_capacity(rope_line.len_chars()))
})
.unwrap())
}
pub fn width_before(&self, line_idx: usize, char_idx: usize) -> usize {
let rope_line = self.rope.line(line_idx);
self.with_cached_column_idx(line_idx, &rope_line, |col| {
col.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.with_cached_column_idx(line_idx, &rope_line, |col| {
col.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.with_cached_column_idx(line_idx, &rope_line, |col| {
col.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.with_cached_column_idx(line_idx, &rope_line, |col| {
col.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.with_cached_column_idx(line_idx, &rope_line, |col| {
col.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.with_cached_column_idx(line_idx, &rope_line, |col| {
col.last_char_until(&self.options, &rope_line, width)
})
}
fn truncate_cached_line_since_char(&self, line_idx: usize, char_idx: usize) {
self
.cached_lines
.borrow_mut()
.retain(|key| key.line_idx != line_idx);
let rope_line = self.rope.line(line_idx);
self.with_cached_column_idx(line_idx, &rope_line, |col| {
col.truncate_since_char(char_idx)
})
}
#[allow(dead_code)]
fn truncate_cached_line_since_width(&self, line_idx: usize, width: usize) {
self
.cached_lines
.borrow_mut()
.retain(|key| key.line_idx != line_idx);
let rope_line = self.rope.line(line_idx);
self.with_cached_column_idx(line_idx, &rope_line, |col| {
col.truncate_since_width(width)
})
}
#[allow(dead_code)]
fn remove_cached_line(&self, line_idx: usize) {
self
.cached_lines
.borrow_mut()
.retain(|key| key.line_idx != line_idx);
self
.cached_width
.borrow_mut()
.retain(|line| *line != line_idx);
}
fn retain_cached_lines<F>(&self, f: F)
where
F: Fn(/* line_idx */ &usize) -> bool,
{
self
.cached_lines
.borrow_mut()
.retain(|key| f(&key.line_idx));
self
.cached_width
.borrow_mut()
.retain(|line_idx| f(line_idx));
}
fn clear_cached_lines(&self) {
self.cached_lines.borrow_mut().clear();
self.cached_width.borrow_mut().clear();
}
#[allow(dead_code)]
fn resize_cached_lines(&self, canvas_size: U16Size) {
self.cached_lines.borrow_mut().resize(canvas_size);
self.cached_width.borrow_mut().resize(canvas_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(&self, _line_idx: usize, _char_idx: usize, _msg: &str) {
}
#[cfg(test)]
fn dbg_print_textline(&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 restore_eol_at_end_if_not_exist(&mut self) {
let eol = Into::<EndOfLineOption>::into(self.options().file_format());
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_rope(self.rope(), 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| *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 to_absolute_char_idx(
&self,
line_idx: usize,
char_idx: 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);
absolute_line_idx + char_idx
}
pub fn to_line_idx_and_char_idx(
&self,
absolute_char_idx: usize,
) -> (/* line_idx */ usize, /* char_idx*/ usize) {
debug_assert!(absolute_char_idx <= self.rope.len_chars());
let line_idx = self.rope.char_to_line(absolute_char_idx);
let line_absolute_char_idx = self.rope.line_to_char(line_idx);
let char_idx = absolute_char_idx - line_absolute_char_idx;
(line_idx, char_idx)
}
fn reset_cache_after_edit(
&mut self,
line_idx: usize,
char_idx: usize,
line_idx_after_edit: usize,
char_idx_after_edit: usize,
) {
if line_idx == line_idx_after_edit {
let truncate_char_idx = std::cmp::min(char_idx_after_edit, char_idx);
self.truncate_cached_line_since_char(
line_idx,
truncate_char_idx.saturating_sub(1),
);
} else {
let truncate_line_idx = std::cmp::min(line_idx_after_edit, line_idx);
self.retain_cached_lines(|line_idx| *line_idx < truncate_line_idx);
}
}
pub fn insert(
&mut self,
line_idx: usize,
char_idx: usize,
payload: CompactString,
) -> (usize, usize) {
let absolute_char_idx = self.to_absolute_char_idx(line_idx, char_idx);
debug_assert_eq!(
self.to_line_idx_and_char_idx(absolute_char_idx).0,
line_idx
);
debug_assert_eq!(
self.to_line_idx_and_char_idx(absolute_char_idx).1,
char_idx
);
self.dbg_print_textline(line_idx, char_idx, "Before insert");
self.rope_mut().insert(absolute_char_idx, payload.as_str());
let absolute_char_idx_after_inserted =
absolute_char_idx + payload.chars().count();
let (line_idx_after_inserted, char_idx_after_inserted) =
self.to_line_idx_and_char_idx(absolute_char_idx_after_inserted);
self.reset_cache_after_edit(
line_idx,
char_idx,
line_idx_after_inserted,
char_idx_after_inserted,
);
if self.options().fix_end_of_line() {
self.restore_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 {
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())
== EndOfLineOption::Crlf.to_compact_string()
{
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())
== EndOfLineOption::Crlf.to_compact_string()
{
i += 2;
} else {
i += 1;
}
acc += 1;
}
std::cmp::min(i, len_chars)
}
pub fn get_removable_char_idx_range(
&self,
line_idx: usize,
char_idx: usize,
n: isize,
) -> Option<Range<usize>> {
if line_idx >= self.rope.len_lines() {
return None;
}
if char_idx > self.rope.line(line_idx).len_chars() {
return None;
}
debug_assert!(char_idx <= self.rope.line(line_idx).len_chars());
let absolute_char_idx = self.to_absolute_char_idx(line_idx, char_idx);
debug_assert_eq!(
self.to_line_idx_and_char_idx(absolute_char_idx).0,
line_idx
);
debug_assert_eq!(
self.to_line_idx_and_char_idx(absolute_char_idx).1,
char_idx
);
self.dbg_print_textline(line_idx, char_idx, "Before delete");
let result = if n > 0 {
let upper = self.n_chars_to_right(absolute_char_idx, n as usize);
debug_assert!(
upper <= self.rope.len_chars(),
"upper ({}) <= self.rope.len_chars() ({})",
upper,
self.rope.len_chars()
);
absolute_char_idx..upper
} else {
let lower = self.n_chars_to_left(absolute_char_idx, (-n) as usize);
lower..absolute_char_idx
};
Some(result)
}
pub fn remove(
&mut self,
line_idx: usize,
char_idx: usize,
n: isize,
) -> Option<(usize, usize)> {
let delete_range = self.get_removable_char_idx_range(line_idx, char_idx, n);
if delete_range.is_none() || delete_range.as_ref().unwrap().is_empty() {
return None;
}
let delete_range = delete_range.unwrap();
self.rope_mut().remove(delete_range.clone());
let absolute_char_idx_after_deleted = delete_range.start;
let absolute_char_idx_after_deleted =
std::cmp::min(absolute_char_idx_after_deleted, self.rope.len_chars());
let (line_idx_after_deleted, char_idx_after_deleted) =
self.to_line_idx_and_char_idx(absolute_char_idx_after_deleted);
self.reset_cache_after_edit(
line_idx,
char_idx,
line_idx_after_deleted,
char_idx_after_deleted,
);
if self.options().fix_end_of_line() {
self.restore_eol_at_end_if_not_exist();
}
self.dbg_print_textline(
line_idx_after_deleted,
char_idx_after_deleted,
"After deleted",
);
Some((line_idx_after_deleted, char_idx_after_deleted))
}
pub fn clear(&mut self) {
self.rope_mut().remove(0..);
self.clear_cached_lines();
}
}