use std::fmt::{Debug, Display, Formatter, Result};
use smallvec::smallvec;
use super::{SelectionList, history::EditorHistory, render_cache::RenderCache, sizing};
use crate::{CachedMemorySize, CaretRaw, CaretScrAdj, ColWidth, DEBUG_TUI_COPY_PASTE,
DEBUG_TUI_MOD, DEFAULT_SYN_HI_FILE_EXT, GCString, GCStringExt, InlineString,
MemoizedMemorySize, MemorySize, RowHeight, RowIndex, ScrOfs, SegString,
Size, TinyInlineString, caret_locate, format_as_kilobytes_with_commas,
glyphs, height, inline_string, ok, row,
validate_buffer_mut::{EditorBufferMutNoDrop, EditorBufferMutWithDrop},
width, with_mut};
#[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: sizing::VecEditorContentLines,
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 {
use super::{DEBUG_TUI_MOD, EditorBuffer, EditorContent, GCStringExt, glyphs,
inline_string, smallvec};
impl EditorBuffer {
#[must_use]
pub fn new_empty(
maybe_file_extension: Option<&str>,
maybe_file_path: Option<&str>,
) -> Self {
let it = Self {
content: EditorContent {
lines: { smallvec!["".grapheme_string()] },
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 {
use super::{DEBUG_TUI_COPY_PASTE, EditorBuffer};
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 {
use super::{CaretRaw, ColWidth, EditorBuffer, RowIndex, ScrOfs, height, sizing,
width};
impl EditorBuffer {
#[must_use]
pub fn get_max_row_index(&self) -> RowIndex {
height(self.get_lines().len()).convert_to_row_index()
}
#[must_use]
pub fn get_line_display_width_at_caret_scr_adj(&self) -> ColWidth {
Self::impl_get_line_display_width_at_caret_scr_adj(
self.get_caret_raw(),
self.get_scr_ofs(),
self.get_lines(),
)
}
#[must_use]
pub fn impl_get_line_display_width_at_caret_scr_adj(
caret_raw: CaretRaw,
scr_ofs: ScrOfs,
lines: &sizing::VecEditorContentLines,
) -> ColWidth {
let caret_scr_adj = caret_raw + scr_ofs;
let row_index = caret_scr_adj.row_index;
let maybe_line_gcs = lines.get(row_index.as_usize());
if let Some(line_gcs) = maybe_line_gcs {
line_gcs.display_width
} else {
width(0)
}
}
#[must_use]
pub fn get_line_display_width_at_row_index(
&self,
row_index: RowIndex,
) -> ColWidth {
Self::impl_get_line_display_width_at_row_index(row_index, self.get_lines())
}
#[must_use]
pub fn impl_get_line_display_width_at_row_index(
row_index: RowIndex,
lines: &sizing::VecEditorContentLines,
) -> ColWidth {
let maybe_line_gcs = lines.get(row_index.as_usize());
if let Some(line_gcs) = maybe_line_gcs {
line_gcs.display_width
} else {
width(0)
}
}
}
}
pub mod content_near_caret {
use super::{EditorBuffer, GCString, SegString, caret_locate, row, width};
impl EditorBuffer {
#[must_use]
pub fn line_at_caret_is_empty(&self) -> bool {
self.get_line_display_width_at_caret_scr_adj() == width(0)
}
#[must_use]
pub fn line_at_caret_scr_adj(&self) -> Option<&GCString> {
if self.is_empty() {
return None;
}
let row_index_scr_adj = self.get_caret_scr_adj().row_index;
let line = self.get_lines().get(row_index_scr_adj.as_usize())?;
Some(line)
}
#[must_use]
pub fn string_at_end_of_line_at_caret_scr_adj(&self) -> Option<SegString> {
if self.is_empty() {
return None;
}
let line = self.line_at_caret_scr_adj()?;
if let caret_locate::CaretColLocationInLine::AtEnd =
caret_locate::locate_col(self)
{
let maybe_last_seg_string = line.get_string_at_end();
return maybe_last_seg_string;
}
None
}
#[must_use]
pub fn string_to_right_of_caret(&self) -> Option<SegString> {
if self.is_empty() {
return None;
}
let line = self.line_at_caret_scr_adj()?;
match caret_locate::locate_col(self) {
caret_locate::CaretColLocationInLine::AtEnd => line.get_string_at_end(),
_ => line.get_string_at_right_of(self.get_caret_scr_adj().col_index),
}
}
#[must_use]
pub fn string_to_left_of_caret(&self) -> Option<SegString> {
if self.is_empty() {
return None;
}
let line = self.line_at_caret_scr_adj()?;
match caret_locate::locate_col(self) {
caret_locate::CaretColLocationInLine::AtEnd => line.get_string_at_end(),
_ => line.get_string_at_left_of(self.get_caret_scr_adj().col_index),
}
}
#[must_use]
pub fn prev_line_above_caret(&self) -> Option<&GCString> {
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 line = self
.get_lines()
.get((row_index_scr_adj - row(1)).as_usize())?;
Some(line)
}
#[must_use]
pub fn string_at_caret(&self) -> Option<SegString> {
if self.is_empty() {
return None;
}
let line = self.line_at_caret_scr_adj()?;
let caret_str_adj_col_index = self.get_caret_scr_adj().col_index;
let seg_string = line.get_string_at(caret_str_adj_col_index)?;
Some(seg_string)
}
#[must_use]
pub fn next_line_below_caret_to_string(&self) -> Option<&GCString> {
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 + row(1);
let line = self.get_lines().get(next_line_row_index.as_usize())?;
Some(line)
}
}
}
pub mod access_and_mutate {
use super::{CaretRaw, CaretScrAdj, DEFAULT_SYN_HI_FILE_EXT, EditorBuffer,
EditorBufferMutNoDrop, EditorBufferMutWithDrop, GCString, GCStringExt,
InlineString, RowHeight, RowIndex, ScrOfs, SelectionList, Size, height,
sizing, with_mut};
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<&GCString> {
self.content.lines.get(row_index.as_usize())
}
#[must_use]
pub fn len(&self) -> RowHeight { height(self.content.lines.len()) }
#[must_use]
pub fn get_lines(&self) -> &sizing::VecEditorContentLines { &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 {
let lines = &self.content.lines;
for (index, line) in lines.iter().enumerate() {
if index > 0 {
acc.push_str(separator);
}
acc.push_str(&line.string);
}
}
)
}
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.as_ref().grapheme_string());
}
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 memory_size_calc_cache {
use super::{CachedMemorySize, EditorBuffer, MemorySize};
use crate::{GetMemSize, MemoizedMemorySize};
impl GetMemSize for EditorBuffer {
fn get_mem_size(&self) -> usize {
self.content.get_mem_size() + self.history.get_mem_size()
}
}
impl CachedMemorySize for EditorBuffer {
fn memory_size_cache(&self) -> &MemoizedMemorySize {
&self.memory_size_calc_cache
}
fn memory_size_cache_mut(&mut self) -> &mut MemoizedMemorySize {
&mut self.memory_size_calc_cache
}
}
impl EditorBuffer {
pub fn invalidate_memory_size_calc_cache(&mut self) {
self.invalidate_memory_size_cache();
self.update_memory_size_cache(); }
pub fn upsert_memory_size_calc_cache(&mut self) {
self.update_memory_size_cache();
}
#[must_use]
pub fn get_memory_size_calc_cached(&mut self) -> MemorySize {
self.get_cached_memory_size()
}
}
}
mod display_impl {
use super::{Display, EditorBuffer, Formatter, MemorySize, Result, ok};
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();
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 {
use super::{Debug, EditorBuffer, EditorContent, Formatter, Result,
format_as_kilobytes_with_commas};
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 {
use crate::GetMemSize;
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(),
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};
#[test]
fn test_cache_invalidated_on_get_mut() {
let mut buffer = 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("More content with lots of text".grapheme_string());
}
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("Even more content".grapheme_string());
}
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::new_empty(Some(DEFAULT_SYN_HI_FILE_EXT), None);
assert_eq2!(buffer.get_lines().len(), 1);
assert!(!buffer.is_empty());
}
#[test]
fn test_is_empty_and_len() {
let mut buffer = 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::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::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::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::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::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::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::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::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("changed".grapheme_string());
}
buffer.add();
assert_eq2!(buffer.history.versions.len(), 2.into());
assert_eq2!(buffer.get_lines()[0], "changed".grapheme_string());
buffer.undo();
assert_eq2!(buffer.get_lines()[0], "initial".grapheme_string());
buffer.redo();
assert_eq2!(buffer.get_lines()[0], "changed".grapheme_string());
buffer.undo();
assert_eq2!(buffer.get_lines()[0], "initial".grapheme_string());
}
}