use super::{SelectionList, history::EditorHistory, render_cache::RenderCache};
use crate::{CaretRaw, CaretScrAdj, ColWidth, CursorPositionBoundsStatus,
DEBUG_TUI_COPY_PASTE, DEBUG_TUI_MOD, DEFAULT_SYN_HI_FILE_EXT,
EditorBufferMutWithDrop, GapBufferLine, GetMemSize, InlineString, LengthOps,
MemoizedMemorySize, MemorySize, NumericValue, RowHeight, RowIndex, ScrOfs,
SegStringOwned, Size, TinyInlineString, ZeroCopyGapBuffer,
caret_locate::locate_col, format_as_kilobytes_with_commas, glyphs,
inline_string, ok, row, validate_buffer_mut::EditorBufferMutNoDrop, width,
with_mut};
use std::fmt::{Debug, Display, Formatter, Result};
#[derive(Clone, PartialEq, Default)]
pub struct EditorBuffer {
pub content: EditorContent,
pub history: EditorHistory,
pub render_cache: RenderCache,
pub memory_size_calc_cache: MemoizedMemorySize,
}
#[derive(Clone, PartialEq, Default)]
pub struct EditorContent {
pub lines: ZeroCopyGapBuffer,
pub caret_raw: CaretRaw,
pub scr_ofs: ScrOfs,
pub maybe_file_extension: Option<TinyInlineString>,
pub maybe_file_path: Option<InlineString>,
pub sel_list: SelectionList,
}
mod construct {
#[allow(clippy::wildcard_imports)]
use super::*;
impl EditorBuffer {
#[must_use]
pub fn new_empty(
maybe_file_extension: Option<&str>,
maybe_file_path: Option<&str>,
) -> Self {
let mut lines = ZeroCopyGapBuffer::default();
lines.push_line("");
let it = Self {
content: EditorContent {
lines,
maybe_file_extension: maybe_file_extension.map(Into::into),
maybe_file_path: maybe_file_path.map(Into::into),
..Default::default()
},
..Default::default()
};
DEBUG_TUI_MOD.then(|| {
tracing::info!(
message = %inline_string!("Construct EditorBuffer {ch}", ch = glyphs::CONSTRUCT_GLYPH),
file_extension = ?maybe_file_extension,
file_path = ?maybe_file_path
);
});
it
}
}
}
pub mod versions {
#[allow(clippy::wildcard_imports)]
use super::*;
impl EditorBuffer {
pub fn add(&mut self) {
self.render_cache.clear();
self.invalidate_memory_size_calc_cache();
let content_copy = self.content.clone();
self.history.add(content_copy);
DEBUG_TUI_COPY_PASTE.then(|| {
tracing::debug!(
message = "🍎🍎🍎 add_content_to_undo_stack buffer",
buffer = ?self
);
});
}
pub fn undo(&mut self) {
self.render_cache.clear();
self.invalidate_memory_size_calc_cache();
if let Some(content) = self.history.undo() {
self.content = content;
}
DEBUG_TUI_COPY_PASTE.then(|| {
tracing::debug!(
message = "🍎🍎🍎 undo buffer",
buffer = ?self
);
});
}
pub fn redo(&mut self) {
self.render_cache.clear();
self.invalidate_memory_size_calc_cache();
if let Some(content) = self.history.redo() {
self.content = content;
}
DEBUG_TUI_COPY_PASTE.then(|| {
tracing::debug!(message = "🍎🍎🍎 redo buffer",
buffer = ?self
);
});
}
}
}
pub mod content_display_width {
#[allow(clippy::wildcard_imports)]
use super::*;
impl EditorBuffer {
#[must_use]
pub fn get_max_row_index(&self) -> RowIndex {
self.get_lines().len().convert_to_index().into()
}
#[must_use]
pub fn get_line_display_width_at_caret_scr_adj(&self) -> ColWidth {
let caret_scr_adj = self.get_caret_raw() + self.get_scr_ofs();
let row_index = caret_scr_adj.row_index;
if let Some(display_width) =
self.get_lines().get_line_display_width(row_index)
{
display_width
} else {
width(0)
}
}
#[must_use]
pub fn get_line_display_width_at_row_index(
&self,
row_index: RowIndex,
) -> ColWidth {
if let Some(display_width) =
self.get_lines().get_line_display_width(row_index)
{
display_width
} else {
width(0)
}
}
}
}
pub mod content_near_caret {
#[allow(clippy::wildcard_imports)]
use super::*;
impl EditorBuffer {
#[must_use]
pub fn line_at_caret_is_empty(&self) -> bool {
self.get_line_display_width_at_caret_scr_adj().is_zero()
}
#[must_use]
pub fn line_at_caret_scr_adj(&self) -> Option<GapBufferLine<'_>> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
self.content.lines.get_line(row_index_scr_adj)
}
#[must_use]
pub fn string_at_end_of_line_at_caret_scr_adj(&self) -> Option<SegStringOwned> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
if let CursorPositionBoundsStatus::AtEnd = locate_col(self) {
if let Some(line_with_info) =
self.content.lines.get_line(row_index_scr_adj)
{
return line_with_info
.info()
.get_string_at_end(line_with_info.content());
}
}
None
}
#[must_use]
pub fn string_to_right_of_caret(&self) -> Option<SegStringOwned> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
let col_index_scr_adj = self.get_caret_scr_adj().col_index;
if let Some(line_with_info) = self.content.lines.get_line(row_index_scr_adj) {
match locate_col(self) {
CursorPositionBoundsStatus::AtEnd => line_with_info
.info()
.get_string_at_end(line_with_info.content()),
_ => line_with_info.info().get_string_at_right_of(
line_with_info.content(),
col_index_scr_adj,
),
}
} else {
None
}
}
#[must_use]
pub fn string_to_left_of_caret(&self) -> Option<SegStringOwned> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
let col_index_scr_adj = self.get_caret_scr_adj().col_index;
if let Some(line_with_info) = self.content.lines.get_line(row_index_scr_adj) {
match locate_col(self) {
CursorPositionBoundsStatus::AtEnd => line_with_info
.info()
.get_string_at_end(line_with_info.content()),
_ => line_with_info.info().get_string_at_left_of(
line_with_info.content(),
col_index_scr_adj,
),
}
} else {
None
}
}
#[must_use]
pub fn prev_line_above_caret(&self) -> Option<&str> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
if row_index_scr_adj == row(0) {
return None;
}
let prev_row_index = row_index_scr_adj - row(1);
self.get_lines().get_line_content(prev_row_index)
}
#[must_use]
pub fn string_at_caret(&self) -> Option<SegStringOwned> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
let col_index_scr_adj = self.get_caret_scr_adj().col_index;
if let Some(line_with_info) = self.content.lines.get_line(row_index_scr_adj) {
line_with_info
.info()
.get_string_at(line_with_info.content(), col_index_scr_adj)
} else {
None
}
}
#[must_use]
pub fn next_line_below_caret_to_string(&self) -> Option<&str> {
if self.is_empty() {
return None;
}
let caret_scr_adj_row_index = self.get_caret_scr_adj().row_index;
let next_line_row_index = caret_scr_adj_row_index + 1;
self.get_lines().get_line_content(next_line_row_index)
}
}
}
pub mod access_and_mutate {
#[allow(clippy::wildcard_imports)]
use super::*;
impl EditorBuffer {
#[must_use]
pub fn is_file_extension_default(&self) -> bool {
match self.content.maybe_file_extension {
Some(ref ext) => ext == DEFAULT_SYN_HI_FILE_EXT,
None => false,
}
}
#[must_use]
pub fn has_file_extension(&self) -> bool {
self.content.maybe_file_extension.is_some()
}
#[must_use]
pub fn get_maybe_file_extension(&self) -> Option<&str> {
match self.content.maybe_file_extension {
Some(ref s) => Some(s.as_str()),
None => None,
}
}
#[must_use]
pub fn is_empty(&self) -> bool { self.content.lines.is_empty() }
#[must_use]
pub fn line_at_row_index(&self, row_index: RowIndex) -> Option<&str> {
self.content.lines.get_line_content(row_index)
}
#[must_use]
pub fn len(&self) -> RowHeight { self.content.lines.len().into() }
#[must_use]
pub fn get_lines(&self) -> &ZeroCopyGapBuffer { &self.content.lines }
#[must_use]
pub fn get_as_string_with_comma_instead_of_newlines(&self) -> InlineString {
self.get_as_string_with_separator(", ")
}
#[must_use]
pub fn get_as_string_with_newlines(&self) -> InlineString {
self.get_as_string_with_separator("\n")
}
#[must_use]
pub fn get_as_string_with_separator(&self, separator: &str) -> InlineString {
with_mut!(
InlineString::new(),
as acc,
run {
for (index, line_with_info) in self.content.lines.iter_lines().enumerate() {
if index > 0 {
acc.push_str(separator);
}
acc.push_str(line_with_info.content());
}
}
)
}
pub fn init_with<I>(&mut self, arg_lines: I)
where
I: IntoIterator,
I::Item: AsRef<str>,
{
self.content.lines.clear();
for line in arg_lines {
self.content.lines.push_line(line.as_ref());
}
self.content.caret_raw = CaretRaw::default();
self.content.scr_ofs = ScrOfs::default();
self.render_cache.clear();
self.invalidate_memory_size_calc_cache();
self.history.clear();
}
#[must_use]
pub fn get_caret_raw(&self) -> CaretRaw { self.content.caret_raw }
#[must_use]
pub fn get_caret_scr_adj(&self) -> CaretScrAdj {
self.content.caret_raw + self.content.scr_ofs
}
#[must_use]
pub fn get_scr_ofs(&self) -> ScrOfs { self.content.scr_ofs }
pub fn get_mut(&mut self, vp: Size) -> EditorBufferMutWithDrop<'_> {
EditorBufferMutWithDrop::new(
&mut self.content.lines,
&mut self.content.caret_raw,
&mut self.content.scr_ofs,
&mut self.content.sel_list,
vp,
&mut self.memory_size_calc_cache,
)
}
pub fn get_mut_no_drop(&mut self, vp: Size) -> EditorBufferMutNoDrop<'_> {
EditorBufferMutNoDrop::new(
&mut self.content.lines,
&mut self.content.caret_raw,
&mut self.content.scr_ofs,
&mut self.content.sel_list,
vp,
&mut self.memory_size_calc_cache,
)
}
#[must_use]
pub fn has_selection(&self) -> bool { !self.content.sel_list.is_empty() }
pub fn clear_selection(&mut self) {
self.content.sel_list.clear();
self.invalidate_memory_size_calc_cache();
}
#[must_use]
pub fn get_selection_list(&self) -> &SelectionList { &self.content.sel_list }
}
}
mod display_impl {
#[allow(clippy::wildcard_imports)]
use super::*;
impl Display for EditorBuffer {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let memory_size = self
.memory_size_calc_cache
.get_cached()
.cloned()
.unwrap_or_else(MemorySize::unknown);
let line_count = self.content.lines.len().as_usize();
let has_selection = self.has_selection();
let caret = self.get_caret_scr_adj();
let line = caret.row_index.as_usize() + 1; let col = caret.col_index.as_usize() + 1;
let ext = self
.content
.maybe_file_extension
.as_ref()
.map_or("txt", |e| e.as_str());
match self.content.maybe_file_path.as_ref() {
Some(path) => {
let file_name = path.rsplit('/').next().unwrap_or("<unnamed>");
write!(f, "editor:{file_name}.{ext}:L{line}:C{col}")?;
}
None => {
write!(f, "editor:<new-buffer>.{ext}:L{line}:C{col}")?;
}
}
if has_selection {
let sel_count = self.content.sel_list.len();
write!(f, ":sel({sel_count}L)")?;
}
write!(f, "[lines={line_count}, size={memory_size}]")?;
ok!()
}
}
}
mod debug_impl {
#[allow(clippy::wildcard_imports)]
use super::*;
impl Debug for EditorBuffer {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"EditorBuffer [
- content: {content:?}
- history: {history:?}
]",
content = self.content,
history = self.history,
)
}
}
impl Debug for EditorContent {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mem_size = self.get_mem_size();
let mem_size_fmt = format_as_kilobytes_with_commas(mem_size);
write! {
f,
"EditorContent [
- lines: {lines:?}, size: {size}
- selection_map: {map}
- ext: {ext:?}, path:{path:?}, caret: {caret:?}, scroll_offset: {scroll:?}
]",
lines = self.lines.len().as_usize(),
size = mem_size_fmt,
ext = self.maybe_file_extension,
caret = self.caret_raw,
map = self.sel_list.to_formatted_string(),
scroll = self.scr_ofs,
path = self.maybe_file_path,
}
}
}
}
#[cfg(test)]
mod test_memory_cache_invalidation {
use super::*;
use crate::{CaretMovementDirection, EditorEngine, RingBuffer, assert_eq2,
caret_scr_adj, col, height, len};
#[test]
fn test_cache_invalidated_on_get_mut() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(Some("md"), None);
let engine = EditorEngine::default();
buffer.init_with(["Hello", "World"]);
buffer.upsert_memory_size_calc_cache(); let initial_memory = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.expect("Cache should have value");
let initial_size = initial_memory.size().expect("Cache should have value");
assert!(initial_size > 0);
{
let buffer_mut = buffer.get_mut(engine.viewport());
buffer_mut
.inner
.lines
.push_line("More content with lots of text");
}
buffer.upsert_memory_size_calc_cache(); let new_memory = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.expect("Cache should have value");
let new_size = new_memory.size().expect("Cache should have value");
assert!(
new_size > initial_size,
"Memory size should increase after adding content"
);
let cached_size = new_size;
{
let buffer_mut_no_drop = buffer.get_mut_no_drop(engine.viewport());
buffer_mut_no_drop
.inner
.lines
.push_line("Even more content");
}
let cached_memory = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.unwrap_or_else(MemorySize::unknown);
assert_eq!(cached_memory.size(), Some(cached_size));
buffer.invalidate_memory_size_calc_cache();
buffer.upsert_memory_size_calc_cache(); let final_memory = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.expect("Cache should have value");
let final_size = final_memory.size().expect("Cache should have value");
assert!(
final_size > new_size,
"Memory size should increase after adding more content"
);
}
#[test]
fn test_editor_empty_state() {
let buffer: EditorBuffer =
EditorBuffer::new_empty(Some(DEFAULT_SYN_HI_FILE_EXT), None);
assert_eq2!(buffer.get_lines().len(), len(1));
assert!(!buffer.is_empty());
}
#[test]
fn test_is_empty_and_len() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
assert!(!buffer.is_empty());
assert_eq2!(buffer.len(), height(1));
buffer.init_with(vec!["line 1", "line 2", "line 3"]);
assert!(!buffer.is_empty());
assert_eq2!(buffer.len(), height(3));
buffer.init_with::<Vec<&str>>(vec![]);
assert!(buffer.is_empty());
assert_eq2!(buffer.len(), height(0));
}
#[test]
fn test_file_extension_functions() {
let buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
assert!(!buffer.has_file_extension());
assert!(!buffer.is_file_extension_default());
assert_eq2!(buffer.get_maybe_file_extension(), None);
let buffer: EditorBuffer =
EditorBuffer::new_empty(Some(DEFAULT_SYN_HI_FILE_EXT), None);
assert!(buffer.has_file_extension());
assert!(buffer.is_file_extension_default());
assert_eq2!(
buffer.get_maybe_file_extension(),
Some(DEFAULT_SYN_HI_FILE_EXT)
);
let buffer: EditorBuffer = EditorBuffer::new_empty(Some("rs"), None);
assert!(buffer.has_file_extension());
assert!(!buffer.is_file_extension_default());
assert_eq2!(buffer.get_maybe_file_extension(), Some("rs"));
}
#[test]
fn test_memory_cache_functions() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
assert!(buffer.memory_size_calc_cache.get_cached().is_none());
buffer.upsert_memory_size_calc_cache();
let initial_cache = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.expect("Cache should be populated");
assert!(initial_cache.size().is_some());
let size_before_invalidate = initial_cache.size().unwrap();
buffer.invalidate_memory_size_calc_cache();
let cache_after_invalidate = buffer
.memory_size_calc_cache
.get_cached()
.cloned()
.expect("Cache should be recalculated after invalidate");
assert_eq!(
cache_after_invalidate.size().unwrap(),
size_before_invalidate
);
let auto_populated = buffer.get_memory_size_calc_cached();
assert!(auto_populated.size().is_some());
assert!(buffer.memory_size_calc_cache.get_cached().is_some());
}
#[test]
fn test_get_mut_invalidates_cache() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
let engine = EditorEngine::default();
buffer.upsert_memory_size_calc_cache();
assert!(buffer.memory_size_calc_cache.get_cached().is_some());
{
let _buffer_mut = buffer.get_mut(engine.viewport());
}
assert!(buffer.memory_size_calc_cache.get_cached().is_none());
}
#[test]
fn test_get_mut_no_drop_preserves_cache() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
let engine = EditorEngine::default();
buffer.upsert_memory_size_calc_cache();
assert!(buffer.get_memory_size_calc_cached().size().is_some());
{
let _buffer_mut_no_drop = buffer.get_mut_no_drop(engine.viewport());
}
assert!(buffer.get_memory_size_calc_cached().size().is_some());
}
#[test]
fn test_clear_selection() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
let engine = EditorEngine::default();
buffer.init_with(vec!["line 1", "line 2"]);
let buffer_mut = buffer.get_mut(engine.viewport());
buffer_mut.inner.sel_list.insert(
row(0),
(
caret_scr_adj(col(0) + row(0)),
caret_scr_adj(col(4) + row(0)),
)
.into(),
CaretMovementDirection::Right,
);
drop(buffer_mut);
assert!(!buffer.get_selection_list().is_empty());
assert_eq2!(buffer.get_selection_list().len(), 1);
buffer.clear_selection();
assert!(buffer.get_selection_list().is_empty());
assert_eq2!(buffer.get_selection_list().len(), 0);
}
#[test]
fn test_history_functions() {
let mut buffer: EditorBuffer = EditorBuffer::new_empty(None, None);
let engine = EditorEngine::default();
buffer.init_with(vec!["initial"]);
buffer.add();
{
let buffer_mut = buffer.get_mut(engine.viewport());
buffer_mut.inner.lines.clear();
buffer_mut.inner.lines.push_line("changed");
}
buffer.add();
assert_eq2!(buffer.history.versions.len(), 2.into());
assert_eq2!(
buffer.get_lines().get_line_content(row(0)).unwrap(),
"changed"
);
buffer.undo();
assert_eq2!(
buffer.get_lines().get_line_content(row(0)).unwrap(),
"initial"
);
buffer.redo();
assert_eq2!(
buffer.get_lines().get_line_content(row(0)).unwrap(),
"changed"
);
buffer.undo();
assert_eq2!(
buffer.get_lines().get_line_content(row(0)).unwrap(),
"initial"
);
}
}