From 1af96c16acf351e9f0cf3e81f04d27fdb9ce8df9 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Sat, 27 Apr 2024 17:40:07 -0500 Subject: [PATCH 01/21] Store layouts in a vec; invalidate them using InvalLines --- src/views/editor/mod.rs | 6 +- src/views/editor/text.rs | 24 +++-- src/views/editor/text_document.rs | 20 +++- src/views/editor/visual_line.rs | 168 +++++++++++++++++++++++++++--- 4 files changed, 191 insertions(+), 27 deletions(-) diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index 4df01185..3c706e32 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -173,7 +173,7 @@ pub struct Editor { pub scroll_to: RwSignal>, /// Holds the cache of the lines and provides many utility functions for them. - lines: Rc, + pub lines: Rc, pub screen_lines: RwSignal, /// Modal mode register @@ -474,7 +474,7 @@ impl Editor { self.doc().receive_char(self, c) } - fn compute_screen_lines(&self, base: RwSignal) -> ScreenLines { + pub fn compute_screen_lines(&self, base: RwSignal) -> ScreenLines { // This function *cannot* access `ScreenLines` with how it is currently implemented. // This is being called from within an update to screen lines. @@ -1480,6 +1480,8 @@ pub fn normal_compute_screen_lines( let cache_rev = editor.doc.get().cache_rev().get(); editor.lines.check_cache_rev(cache_rev); + // println!("Computing screen lines {min_vline:?}..{max_vline:?}; cache rev: {cache_rev}"); + let min_info = editor.iter_vlines(false, min_vline).next(); let mut rvlines = Vec::new(); diff --git a/src/views/editor/text.rs b/src/views/editor/text.rs index 5b015810..3f85884a 100644 --- a/src/views/editor/text.rs +++ b/src/views/editor/text.rs @@ -171,9 +171,9 @@ pub trait Document: DocumentPhantom + Downcast { fn receive_char(&self, ed: &Editor, c: &str); /// Perform a single edit. - fn edit_single(&self, selection: Selection, content: &str, edit_type: EditType) { + fn edit_single(&self, ed: &Editor, selection: Selection, content: &str, edit_type: EditType) { let mut iter = std::iter::once((selection, content)); - self.edit(&mut iter, edit_type); + self.edit(ed, &mut iter, edit_type); } /// Perform the edit(s) on this document. @@ -191,7 +191,12 @@ pub trait Document: DocumentPhantom + Downcast { /// }) /// )) /// ``` - fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType); + fn edit( + &self, + ed: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ); } impl_downcast!(Document); @@ -499,12 +504,17 @@ where self.doc.receive_char(ed, c) } - fn edit_single(&self, selection: Selection, content: &str, edit_type: EditType) { - self.doc.edit_single(selection, content, edit_type) + fn edit_single(&self, ed: &Editor, selection: Selection, content: &str, edit_type: EditType) { + self.doc.edit_single(ed, selection, content, edit_type) } - fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { - self.doc.edit(iter, edit_type) + fn edit( + &self, + ed: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + self.doc.edit(ed, iter, edit_type) } } impl DocumentPhantom for ExtCmdDocument diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs index 4fc74794..23df9364 100644 --- a/src/views/editor/text_document.rs +++ b/src/views/editor/text_document.rs @@ -105,6 +105,10 @@ impl TextDocument { } } + pub fn buffer(&self) -> RwSignal { + self.buffer + } + fn update_cache_rev(&self) { self.cache_rev.try_update(|cache_rev| { *cache_rev += 1; @@ -117,6 +121,13 @@ impl TextDocument { for on_update in on_updates.iter() { on_update(data.clone()); } + + // TODO: check what cases the editor might be `None`... + if let Some(ed) = ed { + for (_, _, inval_lines) in deltas { + ed.lines.invalidate(inval_lines); + } + } } pub fn add_pre_command( @@ -230,7 +241,12 @@ impl Document for TextDocument { } } - fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { + fn edit( + &self, + ed: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { let deltas = self .buffer .try_update(|buffer| buffer.edit(iter, edit_type)); @@ -238,7 +254,7 @@ impl Document for TextDocument { let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]); self.update_cache_rev(); - self.on_update(None, deltas); + self.on_update(Some(ed), deltas); } } impl DocumentPhantom for TextDocument { diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 55ffe176..9996ee70 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -64,7 +64,10 @@ use std::{ }; use floem_editor_core::{ - buffer::rope_text::{RopeText, RopeTextVal}, + buffer::{ + rope_text::{RopeText, RopeTextVal}, + InvalLines, + }, cursor::CursorAffinity, word::WordCursor, }; @@ -121,8 +124,113 @@ impl RVLine { } } -/// (Font Size -> (Buffer Line Number -> Text Layout)) -pub type Layouts = HashMap>>; +/// The cached text layouts. +/// Starts at a specific `base_line`, and then grows from there. +/// This is internally an array, so that newlines and moving the viewport up can be easily handled. +#[derive(Default)] +pub struct Layouts { + base_line: usize, + layouts: Vec>>, +} +impl Layouts { + fn idx(&self, line: usize) -> Option { + line.checked_sub(self.base_line) + } + + pub fn min_line(&self) -> usize { + self.base_line + } + + pub fn max_line(&self) -> Option { + if self.layouts.is_empty() { + None + } else { + Some(self.min_line() + self.layouts.len() - 1) + } + } + + pub fn len(&self) -> usize { + self.layouts.len() + } + + pub fn is_empty(&self) -> bool { + self.layouts.is_empty() + } + + pub fn clear(&mut self) { + self.base_line = 0; + self.layouts.clear(); + } + + pub fn get(&self, line: usize) -> Option<&Arc> { + let idx = self.idx(line)?; + self.layouts.get(idx).map(|x| x.as_ref()).flatten() + } + + pub fn get_mut(&mut self, line: usize) -> Option<&mut Arc> { + let idx = self.idx(line)?; + self.layouts.get_mut(idx).map(|x| x.as_mut()).flatten() + } + + pub fn insert(&mut self, line: usize, layout: Arc) { + if line < self.base_line { + let old_base = self.base_line; + self.base_line = line; + // Resize the layouts at the start to fit the new count + let new_count = old_base - line; + self.layouts + .splice(0..0, std::iter::repeat(None).take(new_count)); + } else if self.layouts.is_empty() { + self.base_line = line; + self.layouts.push(None); + } else if line >= self.base_line + self.layouts.len() { + let new_len = line - self.base_line + 1; + self.layouts.resize(new_len, None); + } + let idx = self.idx(line).unwrap(); + let res = self.layouts.get_mut(idx).unwrap(); + *res = Some(layout); + } + + /// Invalidates the layouts at the given `start_line` for `inval_count` lines. + /// `new_count` is used to know whether to insert new line entries or to remove them, such as + /// for a newline. + pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { + let ib_start_line = start_line.max(self.base_line); + let start_idx = self.idx(ib_start_line).unwrap(); + if start_idx >= self.layouts.len() { + return; + } + + let end_idx = start_idx + inval_count; + let ib_end_idx = end_idx.min(self.layouts.len()); + + for i in start_idx..ib_end_idx { + self.layouts[i] = None; + } + + if new_count == inval_count { + return; + } + + if new_count > inval_count { + let extra = new_count - inval_count; + self.layouts + .splice(ib_end_idx..ib_end_idx, std::iter::repeat(None).take(extra)); + } else { + let remove = inval_count - new_count; + // But remove is not just the difference between inval count and and new count + // As we cut off the end of the interval if it went past the end of the layouts, + let oob_remove = (ib_start_line - start_line) + (ib_end_idx - end_idx); + + let remove = remove - oob_remove; + + // Since we set all the layouts in the interval to None, we can just do the simpler + // task of removing from the start. + self.layouts.drain(start_idx..start_idx + remove); + } + } +} #[derive(Debug, Default, PartialEq, Clone, Copy)] pub struct ConfigId { @@ -149,7 +257,7 @@ pub struct TextLayoutCache { /// Different font-sizes are cached separately, which is useful for features like code lens /// where the font-size can rapidly change. /// It would also be useful for a prospective minimap feature. - pub layouts: Layouts, + pub layouts: HashMap, /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar pub max_width: f64, } @@ -171,13 +279,13 @@ impl TextLayoutCache { } pub fn get(&self, font_size: usize, line: usize) -> Option<&Arc> { - self.layouts.get(&font_size).and_then(|c| c.get(&line)) + self.layouts.get(&font_size).and_then(|c| c.get(line)) } pub fn get_mut(&mut self, font_size: usize, line: usize) -> Option<&mut Arc> { self.layouts .get_mut(&font_size) - .and_then(|c| c.get_mut(&line)) + .and_then(|c| c.get_mut(line)) } /// Get the (start, end) columns of the (line, line_index) @@ -191,6 +299,12 @@ impl TextLayoutCache { self.get(font_size, line) .and_then(|l| l.layout_cols(text_prov, line).nth(line_index)) } + + pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { + for layouts in self.layouts.values_mut() { + layouts.invalidate(start_line, inval_count, new_count); + } + } } // TODO(minor): Should we rename this? It does more than just providing the text layout. It provides the text, text layouts, phantom text, and whether it has multiline phantom text. It is more of an outside state. @@ -288,7 +402,8 @@ pub struct Lines { /// if you were to assign. So this allows us to swap out the `Arc`, though it does mean that /// the other holders of the `Arc` don't get the new version. That is fine currently. pub font_sizes: RefCell>, - text_layouts: Rc>, + #[doc(hidden)] + pub text_layouts: Rc>, wrap: Cell, font_size_cache_id: Cell, last_vline: Rc>>, @@ -472,7 +587,7 @@ impl Lines { .borrow() .layouts .get(&font_size) - .and_then(|f| f.get(&line)) + .and_then(|f| f.get(line)) .cloned() } @@ -874,7 +989,10 @@ impl Lines { (l.cache_rev, l.config_id) }; - if cache_rev != prev_cache_rev || config_id != prev_config_id { + // if cache_rev != prev_cache_rev || config_id != prev_config_id { + // self.clear(cache_rev, Some(config_id)); + // } + if config_id != prev_config_id { self.clear(cache_rev, Some(config_id)); } } @@ -882,9 +1000,9 @@ impl Lines { /// Check whether the text layout cache revision is different. /// Clears the layouts and updates the cache rev if it was different. pub fn check_cache_rev(&self, cache_rev: u64) { - if cache_rev != self.text_layouts.borrow().cache_rev { - self.clear(cache_rev, None); - } + // if cache_rev != self.text_layouts.borrow().cache_rev { + // self.clear(cache_rev, None); + // } } /// Clear the text layouts with a given cache revision @@ -898,6 +1016,23 @@ impl Lines { self.text_layouts.borrow_mut().clear_unchanged(); self.last_vline.set(None); } + + pub fn invalidate(&self, inval_lines: &InvalLines) { + let InvalLines { + start_line, + inval_count, + new_count, + .. + } = *inval_lines; + + if inval_count == 0 { + return; + } + + self.text_layouts + .borrow_mut() + .invalidate(start_line, inval_count, new_count); + } } /// This is a separate function as a hacky solution to lifetimes. @@ -920,7 +1055,7 @@ fn get_init_text_layout( // do it now if !text_layouts.borrow().layouts.contains_key(&font_size) { let mut cache = text_layouts.borrow_mut(); - cache.layouts.insert(font_size, HashMap::new()); + cache.layouts.insert(font_size, Layouts::default()); } // Get whether there's an entry for this specific font size and line @@ -929,7 +1064,7 @@ fn get_init_text_layout( .layouts .get(&font_size) .unwrap() - .get(&line) + .get(line) .is_some(); // If there isn't an entry then we actually have to create it if !cache_exists { @@ -976,7 +1111,7 @@ fn get_init_text_layout( .layouts .get(&font_size) .unwrap() - .get(&line) + .get(line) .cloned() .unwrap() } @@ -1749,8 +1884,9 @@ impl Iterator for VisualLinesRelative { } // TODO: This might skip spaces at the end of lines, which we probably don't want? +#[doc(hidden)] /// Get the end offset of the visual line from the file's line and the line index. -fn end_of_rvline( +pub fn end_of_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, font_size: usize, From fd3edd44d00acf1dd493d53a291cb80c8140693c Mon Sep 17 00:00:00 2001 From: MinusGix Date: Mon, 29 Apr 2024 02:15:54 -0500 Subject: [PATCH 02/21] Microoptimizations are cool --- editor-core/src/buffer/rope_text.rs | 56 ++++++++++++++++++++++++----- src/views/editor/layout.rs | 47 ++++++++++++++++++++---- src/views/editor/visual_line.rs | 52 ++++++++++++++++++++------- 3 files changed, 128 insertions(+), 27 deletions(-) diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs index d8e037a1..18f595d6 100644 --- a/editor-core/src/buffer/rope_text.rs +++ b/editor-core/src/buffer/rope_text.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, ops::Range}; +use std::{borrow::Cow, cell::Cell, ops::Range}; use lapce_xi_rope::{interval::IntervalBounds, rope::ChunkIter, Cursor, Rope}; @@ -79,9 +79,14 @@ pub trait RopeText { /// assert_eq!(text.offset_of_line_col(1, 4), 11); // "d" /// ```` fn offset_of_line_col(&self, line: usize, col: usize) -> usize { + let offset = self.offset_of_line(line); + let last_offset = self.offset_of_line(line + 1); + self.offset_of_offset_col(offset, last_offset, col) + } + + fn offset_of_offset_col(&self, mut offset: usize, last_offset: usize, col: usize) -> usize { let mut pos = 0; - let mut offset = self.offset_of_line(line); - let text = self.slice_to_cow(offset..self.offset_of_line(line + 1)); + let text = self.slice_to_cow(offset..last_offset); let mut iter = text.chars().peekable(); while let Some(c) = iter.next() { // Stop at the end of the line @@ -99,6 +104,12 @@ pub trait RopeText { offset } + fn line_offsets(&self, line: usize) -> (usize, usize) { + let line_start = self.offset_of_line(line); + let line_end = self.line_end_offset(line, true); + (line_start, line_end) + } + fn line_end_col(&self, line: usize, caret: bool) -> usize { let line_start = self.offset_of_line(line); let offset = self.line_end_offset(line, caret); @@ -366,38 +377,67 @@ pub trait RopeText { } } +// We cache the last line. This is cheap to calculate, but is used many times. #[derive(Clone)] pub struct RopeTextVal { - pub text: Rope, + text: Rope, + last_line: Cell>, } impl RopeTextVal { pub fn new(text: Rope) -> Self { - Self { text } + Self { + text, + last_line: Cell::new(None), + } } } impl RopeText for RopeTextVal { fn text(&self) -> &Rope { &self.text } + + fn last_line(&self) -> usize { + if let Some(last_line) = self.last_line.get() { + last_line + } else { + let last_line = self.line_of_offset(self.len()); + self.last_line.set(Some(last_line)); + last_line + } + } } impl From for RopeTextVal { fn from(text: Rope) -> Self { Self::new(text) } } -#[derive(Copy, Clone)] +#[derive(Clone)] pub struct RopeTextRef<'a> { - pub text: &'a Rope, + text: &'a Rope, + last_line: Cell>, } impl<'a> RopeTextRef<'a> { pub fn new(text: &'a Rope) -> Self { - Self { text } + Self { + text, + last_line: Cell::new(None), + } } } impl<'a> RopeText for RopeTextRef<'a> { fn text(&self) -> &Rope { self.text } + + fn last_line(&self) -> usize { + if let Some(last_line) = self.last_line.get() { + last_line + } else { + let last_line = self.line_of_offset(self.len()); + self.last_line.set(Some(last_line)); + last_line + } + } } impl<'a> From<&'a Rope> for RopeTextRef<'a> { fn from(text: &'a Rope) -> Self { diff --git a/src/views/editor/layout.rs b/src/views/editor/layout.rs index 900ffcf4..f643a8e1 100644 --- a/src/views/editor/layout.rs +++ b/src/views/editor/layout.rs @@ -2,7 +2,7 @@ use crate::{ cosmic_text::{LayoutLine, TextLayout}, peniko::Color, }; -use floem_editor_core::buffer::rope_text::RopeText; +use floem_editor_core::buffer::rope_text::{RopeText, RopeTextVal}; use super::{phantom_text::PhantomTextLine, visual_line::TextLayoutProvider}; @@ -53,6 +53,31 @@ impl TextLayoutLine { &'a self, text_prov: impl TextLayoutProvider + 'a, line: usize, + ) -> impl Iterator + 'a { + let text = text_prov.rope_text(); + self.layout_cols_rope(text_prov, text, line) + } + + pub(crate) fn layout_cols_rope<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + text: RopeTextVal, + line: usize, + ) -> impl Iterator + 'a { + let line_offset = text.offset_of_line(line); + // let line_end = text.line_end_col(line, true); + let line_end_offset = text.line_end_offset(line, true); + + self.layout_cols_offsets(text_prov, text, line, line_offset, line_end_offset) + } + + pub(crate) fn layout_cols_offsets<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + text: RopeTextVal, + line: usize, + line_offset: usize, + line_end_offset: usize, ) -> impl Iterator + 'a { let mut prefix = None; // Include an entry if there is nothing @@ -81,18 +106,16 @@ impl TextLayoutLine { let start = line_start + l.glyphs[0].start; let end = line_start + l.glyphs.last().unwrap().end; - let text = text_prov.rope_text(); // We can't just use the original end, because the *true* last glyph on the line // may be a space, but it isn't included in the layout! Though this only happens // for single spaces, for some reason. let pre_end = text_prov.before_phantom_col(line_v, end); - let line_offset = text.offset_of_line(line); - // TODO(minor): We don't really need the entire line, just the two characters after - let line_end = text.line_end_col(line, true); + // TODO: We don't really need the entire line, just the two characters after. This + // could be expensive for large lines. - let end = if pre_end <= line_end { - let after = text.slice_to_cow(line_offset + pre_end..line_offset + line_end); + let end = if pre_end <= line_end_offset - line_offset { + let after = text.slice_to_cow(line_offset + pre_end..line_end_offset); if after.starts_with(' ') && !after.starts_with(" ") { end + 1 } else { @@ -117,6 +140,16 @@ impl TextLayoutLine { self.layout_cols(text_prov, line).map(|(start, _)| start) } + pub(crate) fn start_layout_cols_rope<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + text: &RopeTextVal, + line: usize, + ) -> impl Iterator + 'a { + self.layout_cols_rope(text_prov, text.clone(), line) + .map(|(start, _)| start) + } + /// Get the top y position of the given line index pub fn get_layout_y(&self, nth: usize) -> Option { if nth == 0 { diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 9996ee70..2790e099 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -300,6 +300,22 @@ impl TextLayoutCache { .and_then(|l| l.layout_cols(text_prov, line).nth(line_index)) } + fn get_layout_col_offsets( + &self, + text_prov: &impl TextLayoutProvider, + font_size: usize, + line: usize, + line_index: usize, + line_offset: usize, + line_end_offset: usize, + ) -> Option<(usize, usize)> { + self.get(font_size, line).and_then(|l| { + let text = text_prov.rope_text(); + l.layout_cols_offsets(text_prov, text, line, line_offset, line_end_offset) + .nth(line_index) + }) + } + pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { for layouts in self.layouts.values_mut() { layouts.invalidate(start_line, inval_count, new_count); @@ -1199,7 +1215,8 @@ fn find_rvline_of_offset( // We have to get rvline info for that rvline, so we can get the last line index // This should aways have at least one rvline in it. let font_sizes = lines.font_sizes.borrow(); - let (prev, _) = prev_rvline(&layouts, text_prov, &**font_sizes, rv)?; + let (prev, _) = + prev_rvline(&layouts, text_prov, &rope_text, &**font_sizes, rv)?; return Some(prev); } } @@ -1836,6 +1853,7 @@ impl Iterator for VisualLinesRelative { } let layouts = self.text_layouts.borrow(); + let rope_text = self.text_prov.rope_text(); if self.is_first_iter { // This skips the next line call on the first line. self.is_first_iter = false; @@ -1843,6 +1861,7 @@ impl Iterator for VisualLinesRelative { let v = shift_rvline( &layouts, &self.text_prov, + &rope_text, &*self.font_sizes, self.rvline, self.backwards, @@ -1896,13 +1915,22 @@ pub fn end_of_rvline( return text_prov.text().len(); } - if let Some((_, end_col)) = layouts.get_layout_col(text_prov, font_size, line, line_index) { + let rope_text = text_prov.rope_text(); + let line_offset = rope_text.offset_of_line(line); + let line_end_offset = rope_text.line_end_offset(line, true); + if let Some((_, end_col)) = layouts.get_layout_col_offsets( + text_prov, + font_size, + line, + line_index, + line_offset, + line_end_offset, + ) { let end_col = text_prov.before_phantom_col(line, end_col); - text_prov.rope_text().offset_of_line_col(line, end_col) + let next_line_offset = rope_text.offset_of_line(line + 1); + rope_text.offset_of_offset_col(line_offset, next_line_offset, end_col) } else { - let rope_text = text_prov.rope_text(); - - rope_text.line_end_offset(line, true) + line_end_offset } } @@ -1910,13 +1938,13 @@ pub fn end_of_rvline( fn shift_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, + rope_text: &RopeTextVal, font_sizes: &dyn LineFontSizeProvider, vline: RVLine, backwards: bool, linear: bool, ) -> Option<(RVLine, usize)> { if linear { - let rope_text = text_prov.rope_text(); debug_assert_eq!( vline.line_index, 0, "Line index should be zero if we're linearly working with lines" @@ -1940,10 +1968,10 @@ fn shift_rvline( Some((RVLine::new(next_line, 0), offset)) } } else if backwards { - prev_rvline(layouts, text_prov, font_sizes, vline) + prev_rvline(layouts, text_prov, rope_text, font_sizes, vline) } else { let font_size = font_sizes.font_size(vline.line); - Some(next_rvline(layouts, text_prov, font_size, vline)) + Some(next_rvline(layouts, text_prov, rope_text, font_size, vline)) } } @@ -1971,10 +1999,10 @@ fn rvline_offset( fn next_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, + rope_text: &RopeTextVal, font_size: usize, RVLine { line, line_index }: RVLine, ) -> (RVLine, usize) { - let rope_text = text_prov.rope_text(); if let Some(layout_line) = layouts.get(font_size, line) { if let Some((line_col, _)) = layout_line.layout_cols(text_prov, line).nth(line_index + 1) { let line_col = text_prov.before_phantom_col(line, line_col); @@ -2001,10 +2029,10 @@ fn next_rvline( fn prev_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, + rope_text: &RopeTextVal, font_sizes: &dyn LineFontSizeProvider, RVLine { line, line_index }: RVLine, ) -> Option<(RVLine, usize)> { - let rope_text = text_prov.rope_text(); if line_index == 0 { // Line index was zero so we must be moving back a buffer line if line == 0 { @@ -2015,7 +2043,7 @@ fn prev_rvline( let font_size = font_sizes.font_size(prev_line); if let Some(layout_line) = layouts.get(font_size, prev_line) { let (i, line_col) = layout_line - .start_layout_cols(text_prov, prev_line) + .start_layout_cols_rope(text_prov, rope_text, prev_line) .enumerate() .last() .unwrap_or((0, 0)); From 0d1dedccbfe1ba0a95f789b8a7cd0df5382e20ca Mon Sep 17 00:00:00 2001 From: MinusGix Date: Mon, 29 Apr 2024 21:08:18 -0500 Subject: [PATCH 03/21] Smarter last vline w/ new layout cache Last vline could grow very expensive (10ms) on large (200k lines) files, because it was forced to look up every (font_size, line) in the hashmap. (Possibly it was more expensive than 10ms previously, because it had a second-level hashmap rather than an array!) Now it is quite cheap as the new layout cache makes it simple to only do that iteration on the lines actually in view. It does have the issue that it isn't respecting different font sizes for each line but that's fixable. --- src/views/editor/visual_line.rs | 64 +++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 2790e099..b81f7a03 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -497,29 +497,7 @@ impl Lines { if let Some(last_vline) = self.last_vline.get() { last_vline } else { - // For most files this should easily be fast enough. - // Though it could still be improved. - let rope_text = text_prov.rope_text(); - let hard_line_count = rope_text.num_lines(); - - let line_count = if self.is_linear(text_prov) { - hard_line_count - } else { - let mut soft_line_count = 0; - - let layouts = self.text_layouts.borrow(); - for i in 0..hard_line_count { - let font_size = self.font_size(i); - if let Some(text_layout) = layouts.get(font_size, i) { - let line_count = text_layout.line_count(); - soft_line_count += line_count; - } else { - soft_line_count += 1; - } - } - - soft_line_count - }; + let line_count = self.count_lines(text_prov); let last_vline = line_count.saturating_sub(1); self.last_vline.set(Some(VLine(last_vline))); @@ -527,6 +505,46 @@ impl Lines { } } + fn count_lines(&self, text_prov: impl TextLayoutProvider) -> usize { + let rope_text = text_prov.rope_text(); + let hard_line_count = rope_text.num_lines(); + + if self.is_linear(text_prov) { + return hard_line_count; + } + + let mut soft_line_count = 0; + + // TODO: don't assume font size is constant + // This makes the calculation significantly cheaper for large files... + // Possibly we should have an alternate mode that lets us assume constant font size + let font_size = self.font_size(0); + + let layouts = self.text_layouts.borrow(); + let Some(layouts) = layouts.layouts.get(&font_size) else { + return hard_line_count; + }; + + let base_line = layouts.base_line; + + // Before the layouts baseline, there is #base_line non-wrapped lines + soft_line_count += base_line; + + // Add all the potentially wrapped line counts + for entry in layouts.layouts.iter() { + let line_count = entry.as_ref().map(|l| l.line_count()).unwrap_or(1); + + soft_line_count += line_count; + } + + // Add all the lines after the layouts + let after = base_line + layouts.layouts.len(); + let diff = hard_line_count - after; + soft_line_count += diff; + + soft_line_count + } + /// Clear the cache for the last vline pub fn clear_last_vline(&self) { self.last_vline.set(None); From 38b3a918be37d408a77073dd7ef7a247d5481ade Mon Sep 17 00:00:00 2001 From: MinusGix Date: Mon, 29 Apr 2024 21:12:20 -0500 Subject: [PATCH 04/21] Avoid cloning Register Register could have large text in its fields which we would then clone everytime we performed an action relating to it. --- src/views/editor/actions.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/views/editor/actions.rs b/src/views/editor/actions.rs index bd9ca201..d52e1ae0 100644 --- a/src/views/editor/actions.rs +++ b/src/views/editor/actions.rs @@ -40,7 +40,6 @@ fn handle_edit_command_default( let modal = ed.es.with_untracked(|es| es.modal()); let smart_tab = ed.es.with_untracked(|es| es.smart_tab()); let mut cursor = ed.cursor.get_untracked(); - let mut register = ed.register.get_untracked(); let text = ed.rope_text(); @@ -54,16 +53,18 @@ fn handle_edit_command_default( // modal + smart-tab (etc) if it wants? // That would end up with some duplication of logic, but it would // be more flexible. - let had_edits = action.do_edit(ed, &mut cursor, cmd, modal, &mut register, smart_tab); - if had_edits { - if let Some(data) = yank_data { - register.add_delete(data); + // Avoid cloning register as it may have large data + ed.register.update(|register| { + let had_edits = action.do_edit(ed, &mut cursor, cmd, modal, register, smart_tab); + if had_edits { + if let Some(data) = yank_data { + register.add_delete(data); + } } - } + }); ed.cursor.set(cursor); - ed.register.set(register); CommandExecuted::Yes } @@ -137,12 +138,12 @@ fn handle_motion_mode_command_default( MotionModeCommand::MotionModeYank => MotionMode::Yank { count }, }; let mut cursor = ed.cursor.get_untracked(); - let mut register = ed.register.get_untracked(); - movement::do_motion_mode(ed, action, &mut cursor, motion_mode, &mut register); + ed.register.update(|register| { + movement::do_motion_mode(ed, action, &mut cursor, motion_mode, register); + }); ed.cursor.set(cursor); - ed.register.set(register); CommandExecuted::Yes } From 0914a2370233646c17e5139abe2cac55360bc137 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Mon, 29 Apr 2024 23:40:34 -0500 Subject: [PATCH 05/21] Avoid cloning register --- editor-core/src/editor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/editor-core/src/editor.rs b/editor-core/src/editor.rs index f12409df..3f9632b6 100644 --- a/editor-core/src/editor.rs +++ b/editor-core/src/editor.rs @@ -1047,10 +1047,7 @@ impl Action { } vec![] } - Paste => { - let data = register.unnamed.clone(); - Self::do_paste(cursor, buffer, &data) - } + Paste => Self::do_paste(cursor, buffer, ®ister.unnamed), PasteBefore => { let offset = cursor.offset(); let data = register.unnamed.clone(); From c4c74735c306f11545846c894d1f102bd815f5b9 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 00:05:11 -0500 Subject: [PATCH 06/21] Fix invalidation panic --- src/views/editor/visual_line.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index b81f7a03..899b17ef 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -221,7 +221,9 @@ impl Layouts { let remove = inval_count - new_count; // But remove is not just the difference between inval count and and new count // As we cut off the end of the interval if it went past the end of the layouts, - let oob_remove = (ib_start_line - start_line) + (ib_end_idx - end_idx); + let oob_start = ib_start_line - start_line; + let oob_end = ib_end_idx - end_idx.min(self.layouts.len()); + let oob_remove = oob_start + oob_end; let remove = remove - oob_remove; @@ -263,6 +265,7 @@ pub struct TextLayoutCache { } impl TextLayoutCache { pub fn clear(&mut self, cache_rev: u64, config_id: Option) { + println!("clear {cache_rev}; {config_id:?}"); self.layouts.clear(); if let Some(config_id) = config_id { self.config_id = config_id; @@ -274,6 +277,7 @@ impl TextLayoutCache { /// Clear the layouts without changing the document cache revision. /// Ex: Wrapping width changed, which does not change what the document holds. pub fn clear_unchanged(&mut self) { + println!("clear_unchanged"); self.layouts.clear(); self.max_width = 0.0; } From 782ed12a29a3901aff0f191434672490a0af85b1 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 08:27:11 -0500 Subject: [PATCH 07/21] Better vline search order --- src/views/editor/visual_line.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 899b17ef..356f0aa5 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -265,7 +265,6 @@ pub struct TextLayoutCache { } impl TextLayoutCache { pub fn clear(&mut self, cache_rev: u64, config_id: Option) { - println!("clear {cache_rev}; {config_id:?}"); self.layouts.clear(); if let Some(config_id) = config_id { self.config_id = config_id; @@ -277,7 +276,6 @@ impl TextLayoutCache { /// Clear the layouts without changing the document cache revision. /// Ex: Wrapping width changed, which does not change what the document holds. pub fn clear_unchanged(&mut self) { - println!("clear_unchanged"); self.layouts.clear(); self.max_width = 0.0; } @@ -1413,10 +1411,10 @@ fn find_vline_init_info( } if vline.get() < last_vline.get() / 2 { + find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline) + } else { let last_rvline = lines.last_rvline(text_prov); find_vline_init_info_rv_backward(lines, text_prov, (last_vline, last_rvline), vline) - } else { - find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline) } } From 0ebd029bc021fd31338b3a6f4af32a0a09cbe4a3 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 10:00:31 -0500 Subject: [PATCH 08/21] Remove font size cache This was originally only thought to be used for code lens in Lapce, which would shrink the font size of lines, but I don't believe that's implemented. As well, it should be fast enough to just rerender those text layouts. This makes many operations much faster, no longer having to deal with a hashmap or the trait object indirection every loop iter. --- src/views/editor/visual_line.rs | 252 +++++++++----------------------- 1 file changed, 70 insertions(+), 182 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 356f0aa5..011f3fda 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -58,7 +58,6 @@ use std::{ cell::{Cell, RefCell}, cmp::Ordering, - collections::HashMap, rc::Rc, sync::Arc, }; @@ -255,11 +254,7 @@ pub struct TextLayoutCache { config_id: ConfigId, /// The most recent cache revision of the document. cache_rev: u64, - /// (Font Size -> (Buffer Line Number -> Text Layout)) - /// Different font-sizes are cached separately, which is useful for features like code lens - /// where the font-size can rapidly change. - /// It would also be useful for a prospective minimap feature. - pub layouts: HashMap, + pub layouts: Layouts, /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar pub max_width: f64, } @@ -280,38 +275,34 @@ impl TextLayoutCache { self.max_width = 0.0; } - pub fn get(&self, font_size: usize, line: usize) -> Option<&Arc> { - self.layouts.get(&font_size).and_then(|c| c.get(line)) + pub fn get(&self, line: usize) -> Option<&Arc> { + self.layouts.get(line) } - pub fn get_mut(&mut self, font_size: usize, line: usize) -> Option<&mut Arc> { - self.layouts - .get_mut(&font_size) - .and_then(|c| c.get_mut(line)) + pub fn get_mut(&mut self, line: usize) -> Option<&mut Arc> { + self.layouts.get_mut(line) } /// Get the (start, end) columns of the (line, line_index) pub fn get_layout_col( &self, text_prov: &impl TextLayoutProvider, - font_size: usize, line: usize, line_index: usize, ) -> Option<(usize, usize)> { - self.get(font_size, line) + self.get(line) .and_then(|l| l.layout_cols(text_prov, line).nth(line_index)) } fn get_layout_col_offsets( &self, text_prov: &impl TextLayoutProvider, - font_size: usize, line: usize, line_index: usize, line_offset: usize, line_end_offset: usize, ) -> Option<(usize, usize)> { - self.get(font_size, line).and_then(|l| { + self.get(line).and_then(|l| { let text = text_prov.rope_text(); l.layout_cols_offsets(text_prov, text, line, line_offset, line_end_offset) .nth(line_index) @@ -319,9 +310,7 @@ impl TextLayoutCache { } pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { - for layouts in self.layouts.values_mut() { - layouts.invalidate(start_line, inval_count, new_count); - } + self.layouts.invalidate(start_line, inval_count, new_count); } } @@ -348,12 +337,7 @@ pub trait TextLayoutProvider { // TODO(minor): Do we really need to pass font size to this? The outer-api is providing line // font size provider already, so it should be able to just use that. - fn new_text_layout( - &self, - line: usize, - font_size: usize, - wrap: ResolvedWrap, - ) -> Arc; + fn new_text_layout(&self, line: usize, wrap: ResolvedWrap) -> Arc; /// Translate a column position into the postiion it would be before combining with the phantom /// text @@ -371,13 +355,8 @@ impl TextLayoutProvider for &T { (**self).text() } - fn new_text_layout( - &self, - line: usize, - font_size: usize, - wrap: ResolvedWrap, - ) -> Arc { - (**self).new_text_layout(line, font_size, wrap) + fn new_text_layout(&self, line: usize, wrap: ResolvedWrap) -> Arc { + (**self).new_text_layout(line, wrap) } fn before_phantom_col(&self, line: usize, col: usize) -> usize { @@ -409,7 +388,7 @@ pub trait LineFontSizeProvider { /// events, especially if cache rev gets more specific than clearing everything. #[derive(Debug, Clone, PartialEq)] pub enum LayoutEvent { - CreatedLayout { font_size: usize, line: usize }, + CreatedLayout { line: usize }, } /// The main structure for tracking visual line information. @@ -517,15 +496,8 @@ impl Lines { let mut soft_line_count = 0; - // TODO: don't assume font size is constant - // This makes the calculation significantly cheaper for large files... - // Possibly we should have an alternate mode that lets us assume constant font size - let font_size = self.font_size(0); - let layouts = self.text_layouts.borrow(); - let Some(layouts) = layouts.layouts.get(&font_size) else { - return hard_line_count; - }; + let layouts = &layouts.layouts; let base_line = layouts.base_line; @@ -558,9 +530,8 @@ impl Lines { let rope_text = text_prov.rope_text(); let last_line = rope_text.last_line(); let layouts = self.text_layouts.borrow(); - let font_size = self.font_size(last_line); - if let Some(layout) = layouts.get(font_size, last_line) { + if let Some(layout) = layouts.get(last_line) { let line_count = layout.line_count(); RVLine::new(last_line, line_count - 1) @@ -593,13 +564,11 @@ impl Lines { ) -> Arc { self.check_cache(cache_rev, config_id); - let font_size = self.font_size(line); get_init_text_layout( &self.text_layouts, trigger.then_some(self.layout_event), text_prov, line, - font_size, self.wrap.get(), &self.last_vline, ) @@ -617,14 +586,7 @@ impl Lines { ) -> Option> { self.check_cache(cache_rev, config_id); - let font_size = self.font_size(line); - - self.text_layouts - .borrow() - .layouts - .get(&font_size) - .and_then(|f| f.get(line)) - .cloned() + self.text_layouts.borrow().layouts.get(line).cloned() } /// Initialize the text layout of every line in the real line interval. @@ -733,7 +695,6 @@ impl Lines { } let text_layouts = self.text_layouts.clone(); - let font_sizes = self.font_sizes.clone(); let wrap = self.wrap.get(); let last_vline = self.last_vline.clone(); let layout_event = trigger.then_some(self.layout_event); @@ -743,7 +704,6 @@ impl Lines { // For every (first) vline we initialize the next buffer line's text layout // This ensures it is ready for when re reach it. let next_line = v.rvline.line + 1; - let font_size = font_sizes.borrow().font_size(next_line); // `init_iter_vlines` is the reason `get_init_text_layout` is split out. // Being split out lets us avoid attaching lifetimes to the iterator, since it // only uses Rc/Arcs it is given. @@ -755,7 +715,6 @@ impl Lines { layout_event, &text_prov, next_line, - font_size, wrap, &last_vline, ); @@ -805,7 +764,6 @@ impl Lines { } let text_layouts = self.text_layouts.clone(); - let font_sizes = self.font_sizes.clone(); let wrap = self.wrap.get(); let last_vline = self.last_vline.clone(); let layout_event = trigger.then_some(self.layout_event); @@ -815,7 +773,6 @@ impl Lines { // For every (first) vline we initialize the next buffer line's text layout // This ensures it is ready for when re reach it. let next_line = v.rvline.line + 1; - let font_size = font_sizes.borrow().font_size(next_line); // `init_iter_lines` is the reason `get_init_text_layout` is split out. // Being split out lets us avoid attaching lifetimes to the iterator, since it // only uses Rc/Arcs that it. This is useful since `Lines` would be in a @@ -826,7 +783,6 @@ impl Lines { layout_event, &text_prov, next_line, - font_size, wrap, &last_vline, ); @@ -976,12 +932,11 @@ impl Lines { RVLine { line, line_index }: RVLine, ) -> usize { let rope_text = text_prov.rope_text(); - let font_size = self.font_size(line); let layouts = self.text_layouts.borrow(); // We could remove the debug asserts and allow invalid line indices. However I think it is // desirable to avoid those since they are probably indicative of bugs. - if let Some(text_layout) = layouts.get(font_size, line) { + if let Some(text_layout) = layouts.get(line) { debug_assert!( line_index < text_layout.line_count(), "Line index was out of bounds. This likely indicates keeping an rvline past when it was valid." @@ -1083,28 +1038,14 @@ fn get_init_text_layout( layout_event: Option>, text_prov: impl TextLayoutProvider, line: usize, - font_size: usize, wrap: ResolvedWrap, last_vline: &Cell>, ) -> Arc { - // If we don't have a second layer of the hashmap initialized for this specific font size, - // do it now - if !text_layouts.borrow().layouts.contains_key(&font_size) { - let mut cache = text_layouts.borrow_mut(); - cache.layouts.insert(font_size, Layouts::default()); - } - // Get whether there's an entry for this specific font size and line - let cache_exists = text_layouts - .borrow() - .layouts - .get(&font_size) - .unwrap() - .get(line) - .is_some(); + let cache_exists = text_layouts.borrow().layouts.get(line).is_some(); // If there isn't an entry then we actually have to create it if !cache_exists { - let text_layout = text_prov.new_text_layout(line, font_size, wrap); + let text_layout = text_prov.new_text_layout(line, wrap); // Update last vline if let Some(vline) = last_vline.get() { @@ -1129,27 +1070,16 @@ fn get_init_text_layout( if width > cache.max_width { cache.max_width = width; } - cache - .layouts - .get_mut(&font_size) - .unwrap() - .insert(line, text_layout); + cache.layouts.insert(line, text_layout); } if let Some(layout_event) = layout_event { - layout_event.send(LayoutEvent::CreatedLayout { font_size, line }); + layout_event.send(LayoutEvent::CreatedLayout { line }); } } // Just get the entry, assuming it has been created because we initialize it above. - text_layouts - .borrow() - .layouts - .get(&font_size) - .unwrap() - .get(line) - .cloned() - .unwrap() + text_layouts.borrow().layouts.get(line).cloned().unwrap() } /// Returns (visual line, line_index) @@ -1167,8 +1097,7 @@ fn find_vline_of_offset( let line_start_offset = rope_text.offset_of_line(buffer_line); let vline = find_vline_of_line(lines, text_prov, buffer_line)?; - let font_size = lines.font_size(buffer_line); - let Some(text_layout) = layouts.get(font_size, buffer_line) else { + let Some(text_layout) = layouts.get(buffer_line) else { // No text layout for this line, so the vline we found is definitely correct. // As well, there is no previous soft line to consider return Some((vline, 0)); @@ -1209,8 +1138,7 @@ fn find_rvline_of_offset( let buffer_line = rope_text.line_of_offset(offset); let line_start_offset = rope_text.offset_of_line(buffer_line); - let font_size = lines.font_size(buffer_line); - let Some(text_layout) = layouts.get(font_size, buffer_line) else { + let Some(text_layout) = layouts.get(buffer_line) else { // There is no text layout for this line so the line index is always zero. return Some(RVLine::new(buffer_line, 0)); }; @@ -1234,9 +1162,7 @@ fn find_rvline_of_offset( } else { // We have to get rvline info for that rvline, so we can get the last line index // This should aways have at least one rvline in it. - let font_sizes = lines.font_sizes.borrow(); - let (prev, _) = - prev_rvline(&layouts, text_prov, &rope_text, &**font_sizes, rv)?; + let (prev, _) = prev_rvline(&layouts, text_prov, &rope_text, rv)?; return Some(prev); } } @@ -1330,9 +1256,7 @@ fn find_vline_of_line_backwards( let mut cur_vline = start.get(); for cur_line in line..s_line { - let font_size = lines.font_size(cur_line); - - let Some(text_layout) = layouts.get(font_size, cur_line) else { + let Some(text_layout) = layouts.get(cur_line) else { // no text layout, so its just a normal line cur_vline -= 1; continue; @@ -1362,9 +1286,7 @@ fn find_vline_of_line_forwards( let mut cur_vline = start.get(); for cur_line in s_line..line { - let font_size = lines.font_size(cur_line); - - let Some(text_layout) = layouts.get(font_size, cur_line) else { + let Some(text_layout) = layouts.get(cur_line) else { // no text layout, so its just a normal line cur_vline += 1; continue; @@ -1443,8 +1365,7 @@ fn find_vline_init_info_forward( let layouts = lines.text_layouts.borrow(); while cur_vline < vline.get() { - let font_size = lines.font_size(cur_line); - let line_count = if let Some(text_layout) = layouts.get(font_size, cur_line) { + let line_count = if let Some(text_layout) = layouts.get(cur_line) { let line_count = text_layout.line_count(); // We can then check if the visual line is in this intervening range. @@ -1522,8 +1443,7 @@ fn find_vline_init_info_rv_backward( Ordering::Less => { let line_index = vline.get() - shifted_start.get(); let layouts = lines.text_layouts.borrow(); - let font_size = lines.font_size(start_rvline.line); - if let Some(text_layout) = layouts.get(font_size, start_rvline.line) { + if let Some(text_layout) = layouts.get(start_rvline.line) { vline_init_info_b( text_prov, text_layout, @@ -1562,9 +1482,8 @@ fn find_vline_init_info_backward( } // The target is on this line, so we can just search for it Ordering::Less => { - let font_size = lines.font_size(prev_line); let layouts = lines.text_layouts.borrow(); - if let Some(text_layout) = layouts.get(font_size, prev_line) { + if let Some(text_layout) = layouts.get(prev_line) { return vline_init_info_b( text_prov, text_layout, @@ -1596,8 +1515,7 @@ fn prev_line_start(lines: &Lines, vline: VLine, line: usize) -> Option<(VLine, u let layouts = lines.text_layouts.borrow(); let prev_line = line - 1; - let font_size = lines.font_size(line); - if let Some(layout) = layouts.get(font_size, prev_line) { + if let Some(layout) = layouts.get(prev_line) { let line_count = layout.line_count(); let prev_vline = vline.get() - line_count; Some((VLine(prev_vline), prev_line)) @@ -1802,7 +1720,6 @@ impl Iterator for VisualLines { /// Iterator of the visual lines in a [`Lines`] relative to some starting buffer line. /// This only considers wrapped and phantom text lines that have been rendered into a text layout. struct VisualLinesRelative { - font_sizes: Rc, text_layouts: Rc>, text_prov: T, @@ -1832,13 +1749,11 @@ impl VisualLinesRelative { } let layouts = lines.text_layouts.borrow(); - let font_size = lines.font_size(start.line); - let offset = rvline_offset(&layouts, &text_prov, font_size, start); + let offset = rvline_offset(&layouts, &text_prov, start); let linear = lines.is_linear(&text_prov); VisualLinesRelative { - font_sizes: lines.font_sizes.borrow().clone(), text_layouts: lines.text_layouts.clone(), text_prov, is_done: false, @@ -1852,7 +1767,6 @@ impl VisualLinesRelative { pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLinesRelative { VisualLinesRelative { - font_sizes: lines.font_sizes.borrow().clone(), text_layouts: lines.text_layouts.clone(), text_prov, is_done: true, @@ -1882,7 +1796,6 @@ impl Iterator for VisualLinesRelative { &layouts, &self.text_prov, &rope_text, - &*self.font_sizes, self.rvline, self.backwards, self.linear, @@ -1907,10 +1820,9 @@ impl Iterator for VisualLinesRelative { let start = self.offset; - let font_size = self.font_sizes.font_size(line); - let end = end_of_rvline(&layouts, &self.text_prov, font_size, self.rvline); + let end = end_of_rvline(&layouts, &self.text_prov, self.rvline); - let line_count = if let Some(text_layout) = layouts.get(font_size, line) { + let line_count = if let Some(text_layout) = layouts.get(line) { text_layout.line_count() } else { 1 @@ -1928,7 +1840,6 @@ impl Iterator for VisualLinesRelative { pub fn end_of_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, - font_size: usize, RVLine { line, line_index }: RVLine, ) -> usize { if line > text_prov.rope_text().last_line() { @@ -1938,14 +1849,9 @@ pub fn end_of_rvline( let rope_text = text_prov.rope_text(); let line_offset = rope_text.offset_of_line(line); let line_end_offset = rope_text.line_end_offset(line, true); - if let Some((_, end_col)) = layouts.get_layout_col_offsets( - text_prov, - font_size, - line, - line_index, - line_offset, - line_end_offset, - ) { + if let Some((_, end_col)) = + layouts.get_layout_col_offsets(text_prov, line, line_index, line_offset, line_end_offset) + { let end_col = text_prov.before_phantom_col(line, end_col); let next_line_offset = rope_text.offset_of_line(line + 1); rope_text.offset_of_offset_col(line_offset, next_line_offset, end_col) @@ -1959,7 +1865,6 @@ fn shift_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, rope_text: &RopeTextVal, - font_sizes: &dyn LineFontSizeProvider, vline: RVLine, backwards: bool, linear: bool, @@ -1988,21 +1893,19 @@ fn shift_rvline( Some((RVLine::new(next_line, 0), offset)) } } else if backwards { - prev_rvline(layouts, text_prov, rope_text, font_sizes, vline) + prev_rvline(layouts, text_prov, rope_text, vline) } else { - let font_size = font_sizes.font_size(vline.line); - Some(next_rvline(layouts, text_prov, rope_text, font_size, vline)) + Some(next_rvline(layouts, text_prov, rope_text, vline)) } } fn rvline_offset( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, - font_size: usize, RVLine { line, line_index }: RVLine, ) -> usize { let rope_text = text_prov.rope_text(); - if let Some((line_col, _)) = layouts.get_layout_col(text_prov, font_size, line, line_index) { + if let Some((line_col, _)) = layouts.get_layout_col(text_prov, line, line_index) { let line_col = text_prov.before_phantom_col(line, line_col); rope_text.offset_of_line_col(line, line_col) @@ -2020,10 +1923,9 @@ fn next_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, rope_text: &RopeTextVal, - font_size: usize, RVLine { line, line_index }: RVLine, ) -> (RVLine, usize) { - if let Some(layout_line) = layouts.get(font_size, line) { + if let Some(layout_line) = layouts.get(line) { if let Some((line_col, _)) = layout_line.layout_cols(text_prov, line).nth(line_index + 1) { let line_col = text_prov.before_phantom_col(line, line_col); let offset = rope_text.offset_of_line_col(line, line_col); @@ -2050,7 +1952,6 @@ fn prev_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, rope_text: &RopeTextVal, - font_sizes: &dyn LineFontSizeProvider, RVLine { line, line_index }: RVLine, ) -> Option<(RVLine, usize)> { if line_index == 0 { @@ -2060,8 +1961,7 @@ fn prev_rvline( } let prev_line = line - 1; - let font_size = font_sizes.font_size(prev_line); - if let Some(layout_line) = layouts.get(font_size, prev_line) { + if let Some(layout_line) = layouts.get(prev_line) { let (i, line_col) = layout_line .start_layout_cols_rope(text_prov, rope_text, prev_line) .enumerate() @@ -2080,8 +1980,7 @@ fn prev_rvline( // We're still on the same buffer line, so we can just move to the previous layout/vline. let prev_line_index = line_index - 1; - let font_size = font_sizes.font_size(line); - if let Some(layout_line) = layouts.get(font_size, line) { + if let Some(layout_line) = layouts.get(line) { if let Some((line_col, _)) = layout_line .layout_cols(text_prov, line) .nth(prev_line_index) @@ -2251,12 +2150,7 @@ mod tests { // An implementation relatively close to the actual new text layout impl but simplified. // TODO(minor): It would be nice to just use the same impl as view's - fn new_text_layout( - &self, - line: usize, - font_size: usize, - wrap: ResolvedWrap, - ) -> Arc { + fn new_text_layout(&self, line: usize, wrap: ResolvedWrap) -> Arc { let rope_text = RopeTextRef::new(self.text); let line_content_original = rope_text.line_content(line); @@ -2287,7 +2181,7 @@ mod tests { let attrs = Attrs::new() .family(&self.font_family) - .font_size(font_size as f32); + .font_size(FONT_SIZE as f32); let mut attrs_list = AttrsList::new(attrs); // We don't do line styles, since they aren't relevant @@ -2302,7 +2196,7 @@ mod tests { attrs = attrs.color(fg); } if let Some(phantom_font_size) = phantom.font_size { - attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + attrs = attrs.font_size(phantom_font_size.min(FONT_SIZE) as f32); } attrs_list.add_span(start..end, attrs); // if let Some(font_family) = phantom.font_family.clone() { @@ -2393,7 +2287,7 @@ mod tests { (text, lines) } - fn render_breaks<'a>(text: &'a Rope, lines: &mut Lines, font_size: usize) -> Vec> { + fn render_breaks<'a>(text: &'a Rope, lines: &mut Lines) -> Vec> { // TODO: line_content on ropetextref would have the lifetime reference rope_text // rather than the held &'a Rope. // I think this would require an alternate trait for those functions to avoid incorrect lifetimes. Annoying but workable. @@ -2402,7 +2296,7 @@ mod tests { let layouts = lines.text_layouts.borrow(); for line in 0..rope_text.num_lines() { - if let Some(text_layout) = layouts.get(font_size, line) { + if let Some(text_layout) = layouts.get(line) { let lines = &text_layout.text.lines; for line in lines { let layouts = line.layout_opt().as_deref().unwrap(); @@ -2575,7 +2469,7 @@ mod tests { assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["hello", "world toast and jam", "the end", "hi"] ); @@ -2595,7 +2489,7 @@ mod tests { assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["hello ", "world toast and jam ", "the end ", "hi"] ); } @@ -2666,7 +2560,7 @@ mod tests { lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), [ "greet", "worldhello ", @@ -2749,7 +2643,7 @@ mod tests { lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), [ "hello ", "world toast and jam ", @@ -2809,7 +2703,7 @@ mod tests { assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["hello", "world toast and jam", "the end", "hi"] ); @@ -2818,11 +2712,11 @@ mod tests { { let layouts = lines.text_layouts.borrow(); - assert!(layouts.get(FONT_SIZE, 0).is_some()); - assert!(layouts.get(FONT_SIZE, 1).is_some()); - assert!(layouts.get(FONT_SIZE, 2).is_some()); - assert!(layouts.get(FONT_SIZE, 3).is_some()); - assert!(layouts.get(FONT_SIZE, 4).is_none()); + assert!(layouts.get(0).is_some()); + assert!(layouts.get(1).is_some()); + assert!(layouts.get(2).is_some()); + assert!(layouts.get(3).is_some()); + assert!(layouts.get(4).is_none()); } // start offset, start buffer line, layout line index) @@ -2864,7 +2758,7 @@ mod tests { assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["hello ", "world ", "toast ", "and ", "jam ", "the ", "end ", "hi"] ); @@ -2891,7 +2785,7 @@ mod tests { let (text_prov, mut lines) = make_lines(&text, 2., true); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["aaaa ", "bb ", "bb ", "cc ", "cc ", "dddd ", "eeee ", "ff ", "ff ", "gggg"] ); @@ -2990,7 +2884,7 @@ mod tests { assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), ["hello", "world toast and jam", "the end", "hi"] ); @@ -2999,11 +2893,11 @@ mod tests { { let layouts = lines.text_layouts.borrow(); - assert!(layouts.get(FONT_SIZE, 0).is_some()); - assert!(layouts.get(FONT_SIZE, 1).is_some()); - assert!(layouts.get(FONT_SIZE, 2).is_some()); - assert!(layouts.get(FONT_SIZE, 3).is_some()); - assert!(layouts.get(FONT_SIZE, 4).is_none()); + assert!(layouts.get(0).is_some()); + assert!(layouts.get(1).is_some()); + assert!(layouts.get(2).is_some()); + assert!(layouts.get(3).is_some()); + assert!(layouts.get(4).is_none()); } // start offset, start buffer line, layout line index) @@ -3051,7 +2945,7 @@ mod tests { // An easy way to do this is to always include a space, and then manually cut the glyph // margin in the text layout. assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), + render_breaks(&text, &mut lines), [ "greet ", "worldhello ", @@ -3196,7 +3090,7 @@ mod tests { let text = Rope::from("asdf\nposition: Some(EditorPosition::Offset(self.offset))\nasdf\nasdf"); let (text_prov, mut lines) = make_lines(&text, 1., true); - println!("Breaks: {:?}", render_breaks(&text, &mut lines, FONT_SIZE)); + println!("Breaks: {:?}", render_breaks(&text, &mut lines)); let rvline = lines.rvline_of_offset(&text_prov, 3, CursorAffinity::Backward); assert_eq!(rvline, RVLine::new(0, 0)); @@ -3264,10 +3158,7 @@ mod tests { // The 'hi' is joined with the 'a' so it's not wrapped to a separate line assert_eq!(lines.num_vlines(&text_prov), 4); - assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), - ["ahi ", "b ", "c ", "d "] - ); + assert_eq!(render_breaks(&text, &mut lines), ["ahi ", "b ", "c ", "d "]); let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; // Unchanged. The phantom text has no effect in the position. It doesn't shift a line with @@ -3314,10 +3205,7 @@ mod tests { assert_eq!(lines.num_vlines(&text_prov), 4); // TODO: Should this really be forward rendered? - assert_eq!( - render_breaks(&text, &mut lines, FONT_SIZE), - ["a ", "hib ", "c ", "d "] - ); + assert_eq!(render_breaks(&text, &mut lines), ["a ", "hib ", "c ", "d "]); for (i, v) in vlines.iter().enumerate() { assert_eq!( @@ -3661,7 +3549,7 @@ mod tests { fn test_end_of_rvline() { fn eor(lines: &Lines, text_prov: &impl TextLayoutProvider, rvline: RVLine) -> usize { let layouts = lines.text_layouts.borrow(); - end_of_rvline(&layouts, text_prov, 12, rvline) + end_of_rvline(&layouts, text_prov, rvline) } fn check_equiv(text: &Rope, expected: usize, from: &str) { From e822eff178031d6fa0951adeac00fc86c3de78c7 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 10:06:30 -0500 Subject: [PATCH 09/21] Completely remove font size provider --- src/views/editor/visual_line.rs | 61 ++++----------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 011f3fda..9d94e825 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -368,19 +368,6 @@ impl TextLayoutProvider for &T { } } -pub type FontSizeCacheId = u64; -pub trait LineFontSizeProvider { - /// Get the 'general' font size for a specific buffer line. - /// This is typically the editor font size. - /// There might be alternate font-sizes within the line, like for phantom text, but those are - /// not considered here. - fn font_size(&self, line: usize) -> usize; - - /// An identifier used to mark when the font size info has changed. - /// This lets us update information. - fn cache_id(&self) -> FontSizeCacheId; -} - /// Layout events. This is primarily needed for logic which tracks visual lines intelligently, like /// `ScreenLines` in Lapce. /// This is currently limited to only a `CreatedLayout` event, as changed to the cache rev would @@ -393,27 +380,17 @@ pub enum LayoutEvent { /// The main structure for tracking visual line information. pub struct Lines { - /// This is inside out from the usual way of writing Arc-RefCells due to sometimes wanting to - /// swap out font sizes, while also grabbing an `Arc` to hold. - /// An `Arc>` has the issue that with a `dyn` it can't know they're the same size - /// if you were to assign. So this allows us to swap out the `Arc`, though it does mean that - /// the other holders of the `Arc` don't get the new version. That is fine currently. - pub font_sizes: RefCell>, #[doc(hidden)] pub text_layouts: Rc>, wrap: Cell, - font_size_cache_id: Cell, last_vline: Rc>>, pub layout_event: Listener, } impl Lines { - pub fn new(cx: Scope, font_sizes: RefCell>) -> Lines { - let id = font_sizes.borrow().cache_id(); + pub fn new(cx: Scope) -> Lines { Lines { - font_sizes, text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), wrap: Cell::new(ResolvedWrap::None), - font_size_cache_id: Cell::new(id), last_vline: Rc::new(Cell::new(None)), layout_event: Listener::new_empty(cx), } @@ -461,20 +438,9 @@ impl Lines { self.wrap.get() == ResolvedWrap::None && !text_prov.has_multiline_phantom() } - /// Get the font size that [`Self::font_sizes`] provides - pub fn font_size(&self, line: usize) -> usize { - self.font_sizes.borrow().font_size(line) - } - /// Get the last visual line of the file. /// Cached. pub fn last_vline(&self, text_prov: impl TextLayoutProvider) -> VLine { - let current_id = self.font_sizes.borrow().cache_id(); - if current_id != self.font_size_cache_id.get() { - self.last_vline.set(None); - self.font_size_cache_id.set(current_id); - } - if let Some(last_vline) = self.last_vline.get() { last_vline } else { @@ -2099,7 +2065,7 @@ pub fn hit_position_aff(this: &TextLayout, idx: usize, before: bool) -> HitPosit #[cfg(test)] mod tests { - use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; + use std::{borrow::Cow, collections::HashMap, sync::Arc}; use floem_editor_core::{ buffer::rope_text::{RopeText, RopeTextRef, RopeTextVal}, @@ -2117,8 +2083,8 @@ mod tests { }; use super::{ - find_vline_init_info_forward, find_vline_init_info_rv_backward, ConfigId, FontSizeCacheId, - LineFontSizeProvider, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine, + find_vline_init_info_forward, find_vline_init_info_rv_backward, ConfigId, Lines, RVLine, + ResolvedWrap, TextLayoutProvider, VLine, }; /// For most of the logic we standardize on a specific font size. @@ -2245,19 +2211,6 @@ mod tests { } } - struct TestFontSize { - font_size: usize, - } - impl LineFontSizeProvider for TestFontSize { - fn font_size(&self, _line: usize) -> usize { - self.font_size - } - - fn cache_id(&self) -> FontSizeCacheId { - 0 - } - } - fn make_lines(text: &Rope, width: f32, init: bool) -> (TestTextLayoutProvider<'_>, Lines) { make_lines_ph(text, width, init, HashMap::new()) } @@ -2270,12 +2223,10 @@ mod tests { ) -> (TestTextLayoutProvider<'_>, Lines) { let wrap = Wrap::Word; let r_wrap = ResolvedWrap::Width(width); - let font_sizes = TestFontSize { - font_size: FONT_SIZE, - }; + let text = TestTextLayoutProvider::new(text, ph, wrap); let cx = Scope::new(); - let lines = Lines::new(cx, RefCell::new(Rc::new(font_sizes))); + let lines = Lines::new(cx); lines.set_wrap(r_wrap); if init { From c92009194df1685efef7c4407e14b115e0d76d3b Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 10:11:27 -0500 Subject: [PATCH 10/21] Fixup --- src/views/editor/mod.rs | 79 ++++++----------------------------------- 1 file changed, 10 insertions(+), 69 deletions(-) diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index 3c706e32..ca4601d7 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -1,13 +1,5 @@ use core::indent::IndentStyle; -use std::{ - cell::{Cell, RefCell}, - cmp::Ordering, - collections::{hash_map::DefaultHasher, HashMap}, - hash::{Hash, Hasher}, - rc::Rc, - sync::Arc, - time::Duration, -}; +use std::{cell::Cell, cmp::Ordering, collections::HashMap, rc::Rc, sync::Arc, time::Duration}; use crate::{ action::{exec_after, TimerToken}, @@ -17,7 +9,7 @@ use crate::{ peniko::Color, pointer::{PointerButton, PointerInputEvent, PointerMoveEvent}, prop, prop_extractor, - reactive::{batch, untrack, ReadSignal, RwSignal, Scope}, + reactive::{batch, untrack, RwSignal, Scope}, style::{CursorColor, StylePropValue, TextColor}, view::{IntoView, View}, views::text, @@ -60,8 +52,8 @@ use self::{ text::{Document, Preedit, PreeditData, RenderWhitespace, Styling, WrapMethod}, view::{LineInfo, ScreenLines, ScreenLinesBase}, visual_line::{ - hit_position_aff, ConfigId, FontSizeCacheId, LayoutEvent, LineFontSizeProvider, Lines, - RVLine, ResolvedWrap, TextLayoutProvider, VLine, VLineInfo, + hit_position_aff, ConfigId, LayoutEvent, Lines, RVLine, ResolvedWrap, TextLayoutProvider, + VLine, VLineInfo, }, }; @@ -255,12 +247,7 @@ impl Editor { let doc = cx.create_rw_signal(doc); let style = cx.create_rw_signal(style); - let font_sizes = RefCell::new(Rc::new(EditorFontSizes { - id, - style: style.read_only(), - doc: doc.read_only(), - })); - let lines = Rc::new(Lines::new(cx, font_sizes)); + let lines = Rc::new(Lines::new(cx)); let screen_lines = cx.create_rw_signal(ScreenLines::new(cx, viewport.get_untracked())); let editor_style = cx.create_rw_signal(EditorStyle::default()); @@ -335,11 +322,6 @@ impl Editor { // Get rid of all the effects self.effects_cx.get().dispose(); - *self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes { - id: self.id(), - style: self.style.read_only(), - doc: self.doc.read_only(), - }); self.lines.clear(0, None); self.doc.set(doc); if let Some(styling) = styling { @@ -360,11 +342,6 @@ impl Editor { // Get rid of all the effects self.effects_cx.get().dispose(); - *self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes { - id: self.id(), - style: self.style.read_only(), - doc: self.doc.read_only(), - }); self.lines.clear(0, None); self.style.set(styling); @@ -1189,12 +1166,7 @@ impl TextLayoutProvider for Editor { Editor::text(self) } - fn new_text_layout( - &self, - line: usize, - _font_size: usize, - _wrap: ResolvedWrap, - ) -> Arc { + fn new_text_layout(&self, line: usize, _wrap: ResolvedWrap) -> Arc { // TODO: we could share text layouts between different editor views given some knowledge of // their wrapping let edid = self.id(); @@ -1289,13 +1261,9 @@ impl TextLayoutProvider for Editor { let indent = if indent_line != line { // TODO: This creates the layout if it isn't already cached, but it doesn't cache the // result because the current method of managing the cache is not very smart. - let layout = self.try_get_text_layout(indent_line).unwrap_or_else(|| { - self.new_text_layout( - indent_line, - style.font_size(edid, indent_line), - self.lines.wrap(), - ) - }); + let layout = self + .try_get_text_layout(indent_line) + .unwrap_or_else(|| self.new_text_layout(indent_line, self.lines.wrap())); layout.indent + 1.0 } else { let offset = text.first_non_blank_character_on_line(indent_line); @@ -1328,31 +1296,6 @@ impl TextLayoutProvider for Editor { } } -struct EditorFontSizes { - id: EditorId, - style: ReadSignal>, - doc: ReadSignal>, -} -impl LineFontSizeProvider for EditorFontSizes { - fn font_size(&self, line: usize) -> usize { - self.style - .with_untracked(|style| style.font_size(self.id, line)) - } - - fn cache_id(&self) -> FontSizeCacheId { - let mut hasher = DefaultHasher::new(); - - // TODO: is this actually good enough for comparing cache state? - // We could just have it return an arbitrary type that impl's Eq? - self.style - .with_untracked(|style| style.id().hash(&mut hasher)); - self.doc - .with_untracked(|doc| doc.cache_rev().get_untracked().hash(&mut hasher)); - - hasher.finish() - } -} - /// Minimum width that we'll allow the view to be wrapped at. const MIN_WRAPPED_WIDTH: f32 = 100.0; @@ -1395,7 +1338,7 @@ fn create_view_effects(cx: Scope, ed: &Editor) { // function, to avoid getting confused about what is relevant where. match val { - LayoutEvent::CreatedLayout { line, .. } => { + LayoutEvent::CreatedLayout { line } => { let sl = ed.screen_lines.get_untracked(); // Intelligently update screen lines, avoiding recalculation if possible @@ -1480,8 +1423,6 @@ pub fn normal_compute_screen_lines( let cache_rev = editor.doc.get().cache_rev().get(); editor.lines.check_cache_rev(cache_rev); - // println!("Computing screen lines {min_vline:?}..{max_vline:?}; cache rev: {cache_rev}"); - let min_info = editor.iter_vlines(false, min_vline).next(); let mut rvlines = Vec::new(); From 2a3c2b8121fec92da73fc1b0b227ad7a0c98286a Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 10:27:28 -0500 Subject: [PATCH 11/21] Avoid possible allocation --- editor-core/src/buffer/rope_text.rs | 5 +++++ src/views/editor/layout.rs | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs index 18f595d6..f6315ef1 100644 --- a/editor-core/src/buffer/rope_text.rs +++ b/editor-core/src/buffer/rope_text.rs @@ -239,6 +239,11 @@ pub trait RopeText { .slice_to_cow(range.start.min(self.len())..range.end.min(self.len())) } + fn chars(&self, range: Range) -> impl Iterator + '_ { + let iter = self.text().iter_chunks(range); + iter.flat_map(str::chars) + } + // TODO(minor): Once you can have an `impl Trait` return type in a trait, this could use that. /// Iterate over (utf8_offset, char) values in the given range #[allow(clippy::type_complexity)] diff --git a/src/views/editor/layout.rs b/src/views/editor/layout.rs index f643a8e1..7a7a994a 100644 --- a/src/views/editor/layout.rs +++ b/src/views/editor/layout.rs @@ -111,12 +111,9 @@ impl TextLayoutLine { // for single spaces, for some reason. let pre_end = text_prov.before_phantom_col(line_v, end); - // TODO: We don't really need the entire line, just the two characters after. This - // could be expensive for large lines. - let end = if pre_end <= line_end_offset - line_offset { - let after = text.slice_to_cow(line_offset + pre_end..line_end_offset); - if after.starts_with(' ') && !after.starts_with(" ") { + let mut after = text.chars(line_offset + pre_end..line_end_offset); + if after.next() == Some(' ') && after.next() != Some(' ') { end + 1 } else { end From 05dca1a1c329aeff56893a3acb5115b8fbbc6a33 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 11:03:26 -0500 Subject: [PATCH 12/21] Avoid using line_content --- editor-core/src/buffer/rope_text.rs | 58 +++++++++++++++++++++++++---- editor-core/src/editor.rs | 6 +-- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs index f6315ef1..92b280ab 100644 --- a/editor-core/src/buffer/rope_text.rs +++ b/editor-core/src/buffer/rope_text.rs @@ -132,21 +132,65 @@ pub trait RopeText { /// assert_eq!(text.line_end_offset(2, false), 11); // "world|" /// ``` fn line_end_offset(&self, line: usize, caret: bool) -> usize { - let mut offset = self.offset_of_line(line + 1); - let mut line_content: &str = &self.line_content(line); - if line_content.ends_with("\r\n") { + let start_offset = self.offset_of_line(line); + let end_offset = self.offset_of_line(line + 1); + + let mut offset = end_offset; + + let start = end_offset.saturating_sub(2).max(start_offset); + let mut chars = self.chars(start..end_offset); + let fst = chars.next(); + let snd = chars.next(); + + if fst == Some('\r') && snd == Some('\n') { offset -= 2; - line_content = &line_content[..line_content.len() - 2]; - } else if line_content.ends_with('\n') { + } else if (fst == Some('\n') && snd == None) || snd == Some('\n') { offset -= 1; - line_content = &line_content[..line_content.len() - 1]; } - if !caret && !line_content.is_empty() { + + if !caret && start_offset != offset { offset = self.prev_grapheme_offset(offset, 1, 0); } offset } + /// Whether the line is completely empty + /// This counts both 'empty' and 'only has newline' + /// ```rust + /// # use floem_editor_core::xi_rope::Rope; + /// # use floem_editor_core::buffer::rope_text::{RopeText, RopeTextRef}; + /// let text = Rope::from("hello\nworld toast and jam\n\nhi"); + /// let text = RopeTextRef::new(&text); + /// assert_eq!(text.is_line_empty(0), false); + /// assert_eq!(text.is_line_empty(1), false); + /// assert_eq!(text.is_line_empty(2), true); + /// assert_eq!(text.is_line_empty(3), false); + /// + /// let text = Rope::from(""); + /// let text = RopeTextRef::new(&text); + /// assert_eq!(text.is_line_empty(0), true); + /// ``` + fn is_line_empty(&self, line: usize) -> bool { + let start_offset = self.offset_of_line(line); + let end_offset = self.offset_of_line(line + 1); + + if start_offset == end_offset { + return true; + } + + let mut chars = self.chars(start_offset..end_offset); + let fst = chars.next(); + let snd = chars.next(); + + if fst == Some('\r') && snd == Some('\n') { + true + } else if (fst == Some('\n') && snd == None) || snd == Some('\n') { + true + } else { + false + } + } + /// Returns the content of the given line. /// Includes the line ending if it exists. (-> the last line won't have a line ending) /// Lines past the end of the document will return an empty string. diff --git a/editor-core/src/editor.rs b/editor-core/src/editor.rs index 3f9632b6..ab98179e 100644 --- a/editor-core/src/editor.rs +++ b/editor-core/src/editor.rs @@ -654,8 +654,7 @@ impl Action { } for line in start_line..=end_line { if lines.insert(line) { - let line_content = buffer.line_content(line); - if line_content == "\n" || line_content == "\r\n" { + if buffer.is_line_empty(line) { continue; } let nonblank = buffer.first_non_blank_character_on_line(line); @@ -684,8 +683,7 @@ impl Action { } for line in start_line..=end_line { if lines.insert(line) { - let line_content = buffer.line_content(line); - if line_content == "\n" || line_content == "\r\n" { + if buffer.is_line_empty(line) { continue; } let nonblank = buffer.first_non_blank_character_on_line(line); From 569cbebde425fd6787d5bc7ba827b51d44197c83 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 11:07:07 -0500 Subject: [PATCH 13/21] Another minor avoidance of possible alloc --- editor-core/src/buffer/rope_text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs index 92b280ab..8020fc9b 100644 --- a/editor-core/src/buffer/rope_text.rs +++ b/editor-core/src/buffer/rope_text.rs @@ -86,8 +86,7 @@ pub trait RopeText { fn offset_of_offset_col(&self, mut offset: usize, last_offset: usize, col: usize) -> usize { let mut pos = 0; - let text = self.slice_to_cow(offset..last_offset); - let mut iter = text.chars().peekable(); + let mut iter = self.chars(offset..last_offset).peekable(); while let Some(c) = iter.next() { // Stop at the end of the line if c == '\n' || (c == '\r' && iter.peek() == Some(&'\n')) { From 97e01461032ed3339016578f9a4dea639ac7355f Mon Sep 17 00:00:00 2001 From: MinusGix Date: Tue, 30 Apr 2024 11:20:39 -0500 Subject: [PATCH 14/21] Avoid some next line calculation --- editor-core/src/buffer/rope_text.rs | 52 ++++++++++++++++------------- src/views/editor/layout.rs | 4 +-- src/views/editor/visual_line.rs | 4 +-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs index 8020fc9b..155d1106 100644 --- a/editor-core/src/buffer/rope_text.rs +++ b/editor-core/src/buffer/rope_text.rs @@ -103,10 +103,33 @@ pub trait RopeText { offset } - fn line_offsets(&self, line: usize) -> (usize, usize) { - let line_start = self.offset_of_line(line); - let line_end = self.line_end_offset(line, true); - (line_start, line_end) + /// (start_line_offset, line_end_offset(caret), line+1 offset) + /// Which we can provide as we need the first and last to compute the second. + fn line_offsets(&self, line: usize, caret: bool) -> (usize, usize, usize) { + // let line_start = self.offset_of_line(line); + // let line_end = self.line_end_offset(line, true); + // (line_start, line_end) + + let start_offset = self.offset_of_line(line); + let end_offset = self.offset_of_line(line + 1); + + let mut offset = end_offset; + + let start = end_offset.saturating_sub(2).max(start_offset); + let mut chars = self.chars(start..end_offset); + let fst = chars.next(); + let snd = chars.next(); + + if fst == Some('\r') && snd == Some('\n') { + offset -= 2; + } else if (fst == Some('\n') && snd == None) || snd == Some('\n') { + offset -= 1; + } + + if !caret && start_offset != offset { + offset = self.prev_grapheme_offset(offset, 1, 0); + } + (start_offset, offset, end_offset) } fn line_end_col(&self, line: usize, caret: bool) -> usize { @@ -131,26 +154,7 @@ pub trait RopeText { /// assert_eq!(text.line_end_offset(2, false), 11); // "world|" /// ``` fn line_end_offset(&self, line: usize, caret: bool) -> usize { - let start_offset = self.offset_of_line(line); - let end_offset = self.offset_of_line(line + 1); - - let mut offset = end_offset; - - let start = end_offset.saturating_sub(2).max(start_offset); - let mut chars = self.chars(start..end_offset); - let fst = chars.next(); - let snd = chars.next(); - - if fst == Some('\r') && snd == Some('\n') { - offset -= 2; - } else if (fst == Some('\n') && snd == None) || snd == Some('\n') { - offset -= 1; - } - - if !caret && start_offset != offset { - offset = self.prev_grapheme_offset(offset, 1, 0); - } - offset + self.line_offsets(line, caret).1 } /// Whether the line is completely empty diff --git a/src/views/editor/layout.rs b/src/views/editor/layout.rs index 7a7a994a..43f19ac3 100644 --- a/src/views/editor/layout.rs +++ b/src/views/editor/layout.rs @@ -64,9 +64,7 @@ impl TextLayoutLine { text: RopeTextVal, line: usize, ) -> impl Iterator + 'a { - let line_offset = text.offset_of_line(line); - // let line_end = text.line_end_col(line, true); - let line_end_offset = text.line_end_offset(line, true); + let (line_offset, line_end_offset, _) = text.line_offsets(line, true); self.layout_cols_offsets(text_prov, text, line, line_offset, line_end_offset) } diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 9d94e825..10a1fcbe 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -1813,13 +1813,11 @@ pub fn end_of_rvline( } let rope_text = text_prov.rope_text(); - let line_offset = rope_text.offset_of_line(line); - let line_end_offset = rope_text.line_end_offset(line, true); + let (line_offset, line_end_offset, next_line_offset) = rope_text.line_offsets(line, true); if let Some((_, end_col)) = layouts.get_layout_col_offsets(text_prov, line, line_index, line_offset, line_end_offset) { let end_col = text_prov.before_phantom_col(line, end_col); - let next_line_offset = rope_text.offset_of_line(line + 1); rope_text.offset_of_offset_col(line_offset, next_line_offset, end_col) } else { line_end_offset From c9f825a998f424c35b9e8eb3d7702a6cd17c90fd Mon Sep 17 00:00:00 2001 From: MinusGix Date: Wed, 1 May 2024 01:56:50 -0500 Subject: [PATCH 15/21] next_rvline microopt --- src/views/editor/visual_line.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 10a1fcbe..c2012abe 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -1883,16 +1883,26 @@ fn rvline_offset( /// Move to the next visual line, giving the new information. /// Returns `(new rel vline, offset)` -fn next_rvline( +pub fn next_rvline( layouts: &TextLayoutCache, text_prov: &impl TextLayoutProvider, rope_text: &RopeTextVal, RVLine { line, line_index }: RVLine, ) -> (RVLine, usize) { if let Some(layout_line) = layouts.get(line) { - if let Some((line_col, _)) = layout_line.layout_cols(text_prov, line).nth(line_index + 1) { + let (line_offset, line_end_offset, next_line_offset) = rope_text.line_offsets(line, true); + if let Some((line_col, _)) = layout_line + .layout_cols_offsets( + text_prov, + rope_text.clone(), + line, + line_offset, + line_end_offset, + ) + .nth(line_index + 1) + { let line_col = text_prov.before_phantom_col(line, line_col); - let offset = rope_text.offset_of_line_col(line, line_col); + let offset = rope_text.offset_of_offset_col(line_offset, next_line_offset, line_col); (RVLine::new(line, line_index + 1), offset) } else { From 75780da6666a7b28088375f7320cd1192ace8b46 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Wed, 1 May 2024 06:14:19 -0500 Subject: [PATCH 16/21] Simplify vline calculation This significantly simplifies & optimizes the calculation of the initial visual line, now being mostly only sensitive to how many lines are on screen than the size of the file. As well it lets us get rid of a bunch of side code that isn't needed. --- src/views/editor/visual_line.rs | 405 ++++++-------------------------- 1 file changed, 68 insertions(+), 337 deletions(-) diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index c2012abe..d2601867 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -1271,7 +1271,7 @@ fn find_vline_of_line_forwards( /// phantom text. /// /// Returns `None` if the visual line is out of bounds. -fn find_vline_init_info( +pub fn find_vline_init_info( lines: &Lines, text_prov: &impl TextLayoutProvider, vline: VLine, @@ -1298,216 +1298,60 @@ fn find_vline_init_info( return None; } - if vline.get() < last_vline.get() / 2 { - find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline) - } else { - let last_rvline = lines.last_rvline(text_prov); - find_vline_init_info_rv_backward(lines, text_prov, (last_vline, last_rvline), vline) - } -} + let layouts = lines.text_layouts.borrow(); + let layouts = &layouts.layouts; + let base_line = layouts.base_line; -// TODO(minor): should we package (VLine, buffer line) into a struct since we use it for these -// pseudo relative calculations often? -/// Find the `(start offset, rvline)` of a given [`VLine`] -/// -/// start offset is into the file, rather than text layout's string, so it does not include -/// phantom text. -/// -/// Returns `None` if the visual line is out of bounds, or if the start is past our target. -fn find_vline_init_info_forward( - lines: &Lines, - text_prov: &impl TextLayoutProvider, - (start, start_line): (VLine, usize), - vline: VLine, -) -> Option<(usize, RVLine)> { - if start > vline { - return None; + if base_line > vline.get() { + // The vline is not within the rendered, thus it is linear + let line = vline.get(); + return Some((rope_text.offset_of_line(line), RVLine::new(line, 0))); } - let rope_text = text_prov.rope_text(); + let mut cur_vline = base_line; + for (i, entry) in layouts.layouts.iter().enumerate() { + let cur_line = base_line + i; - let mut cur_line = start_line; - let mut cur_vline = start.get(); + let line_count = entry.as_ref().map(|l| l.line_count()).unwrap_or(1); - let layouts = lines.text_layouts.borrow(); - while cur_vline < vline.get() { - let line_count = if let Some(text_layout) = layouts.get(cur_line) { - let line_count = text_layout.line_count(); - - // We can then check if the visual line is in this intervening range. - if cur_vline + line_count > vline.get() { - // We found the line that contains the visual line. - // We can now find the offset of the visual line within the line. - let line_index = vline.get() - cur_vline; - // TODO: is it fine to unwrap here? - let col = text_layout + if cur_vline + line_count > vline.get() { + let line_index = vline.get() - cur_vline; + let col = if let Some(entry) = &entry { + let col = entry .start_layout_cols(text_prov, cur_line) .nth(line_index) .unwrap_or(0); - let col = text_prov.before_phantom_col(cur_line, col); - - let offset = rope_text.offset_of_line_col(cur_line, col); - return Some((offset, RVLine::new(cur_line, line_index))); - } - - // The visual line is not in this line, so we have to keep looking. - line_count - } else { - // There was no text layout so we only have to consider the line breaks in this line. - // Which, since we don't handle phantom text, is just one. + text_prov.before_phantom_col(cur_line, col) + } else { + 0 + }; - 1 - }; + let offset = rope_text.offset_of_line_col(cur_line, col); + return Some((offset, RVLine::new(cur_line, line_index))); + } - cur_line += 1; cur_vline += line_count; } - // We've reached the visual line we're looking for, we can return the offset. - // This also handles the case where the vline is past the end of the text. + let cur_line = base_line + layouts.layouts.len(); + + let linear_diff = vline.get() - cur_vline; + + let cur_vline = cur_line + linear_diff; + let cur_line = cur_line + linear_diff; + if cur_vline == vline.get() { if cur_line > rope_text.last_line() { return None; } - // We use cur_line because if our target vline is out of bounds - // then the result should be len Some((rope_text.offset_of_line(cur_line), RVLine::new(cur_line, 0))) } else { - // We've gone past the visual line we're looking for, so it is out of bounds. + // We've gone past the visual line we are looking for, so it is out of bounds. None } } -/// Find the `(start offset, rvline)` of a given [`VLine`] -/// -/// `start offset` is into the file, rather than the text layout's content, so it does not -/// include phantom text. -/// -/// Returns `None` if the visual line is out of bounds or if the start is before our target. -/// This iterates backwards. -fn find_vline_init_info_rv_backward( - lines: &Lines, - text_prov: &impl TextLayoutProvider, - (start, start_rvline): (VLine, RVLine), - vline: VLine, -) -> Option<(usize, RVLine)> { - if start < vline { - // The start was before the target. - return None; - } - - // This would the vline at the very start of the buffer line - let shifted_start = VLine(start.get() - start_rvline.line_index); - match shifted_start.cmp(&vline) { - // The shifted start was equivalent to the vline, which makes it easy to compute - Ordering::Equal => { - let offset = text_prov.rope_text().offset_of_line(start_rvline.line); - Some((offset, RVLine::new(start_rvline.line, 0))) - } - // The new start is before the vline, that means the vline is on the same line. - Ordering::Less => { - let line_index = vline.get() - shifted_start.get(); - let layouts = lines.text_layouts.borrow(); - if let Some(text_layout) = layouts.get(start_rvline.line) { - vline_init_info_b( - text_prov, - text_layout, - RVLine::new(start_rvline.line, line_index), - ) - } else { - // There was no text layout so we only have to consider the line breaks in this line. - - let base_offset = text_prov.rope_text().offset_of_line(start_rvline.line); - Some((base_offset, RVLine::new(start_rvline.line, 0))) - } - } - Ordering::Greater => find_vline_init_info_backward( - lines, - text_prov, - (shifted_start, start_rvline.line), - vline, - ), - } -} - -fn find_vline_init_info_backward( - lines: &Lines, - text_prov: &impl TextLayoutProvider, - (mut start, mut start_line): (VLine, usize), - vline: VLine, -) -> Option<(usize, RVLine)> { - loop { - let (prev_vline, prev_line) = prev_line_start(lines, start, start_line)?; - - match prev_vline.cmp(&vline) { - // We found the target, and it was at the start - Ordering::Equal => { - let offset = text_prov.rope_text().offset_of_line(prev_line); - return Some((offset, RVLine::new(prev_line, 0))); - } - // The target is on this line, so we can just search for it - Ordering::Less => { - let layouts = lines.text_layouts.borrow(); - if let Some(text_layout) = layouts.get(prev_line) { - return vline_init_info_b( - text_prov, - text_layout, - RVLine::new(prev_line, vline.get() - prev_vline.get()), - ); - } else { - // There was no text layout so we only have to consider the line breaks in this line. - // Which, since we don't handle phantom text, is just one. - - let base_offset = text_prov.rope_text().offset_of_line(prev_line); - return Some((base_offset, RVLine::new(prev_line, 0))); - } - } - // The target is before this line, so we have to keep searching - Ordering::Greater => { - start = prev_vline; - start_line = prev_line; - } - } - } -} - -/// Get the previous (line, start visual line) from a (line, start visual line). -fn prev_line_start(lines: &Lines, vline: VLine, line: usize) -> Option<(VLine, usize)> { - if line == 0 { - return None; - } - - let layouts = lines.text_layouts.borrow(); - - let prev_line = line - 1; - if let Some(layout) = layouts.get(prev_line) { - let line_count = layout.line_count(); - let prev_vline = vline.get() - line_count; - Some((VLine(prev_vline), prev_line)) - } else { - // There's no layout for the previous line which makes this easy - Some((VLine(vline.get() - 1), prev_line)) - } -} - -fn vline_init_info_b( - text_prov: &impl TextLayoutProvider, - text_layout: &TextLayoutLine, - rv: RVLine, -) -> Option<(usize, RVLine)> { - let rope_text = text_prov.rope_text(); - let col = text_layout - .start_layout_cols(text_prov, rv.line) - .nth(rv.line_index) - .unwrap_or(0); - let col = text_prov.before_phantom_col(rv.line, col); - - let offset = rope_text.offset_of_line_col(rv.line, col); - - Some((offset, rv)) -} - /// Information about the visual line and how it relates to the underlying buffer line. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] @@ -2091,8 +1935,7 @@ mod tests { }; use super::{ - find_vline_init_info_forward, find_vline_init_info_rv_backward, ConfigId, Lines, RVLine, - ResolvedWrap, TextLayoutProvider, VLine, + find_vline_init_info, ConfigId, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine, }; /// For most of the logic we standardize on a specific font size. @@ -2315,22 +2158,12 @@ mod tests { } } - fn ffvline_info( - lines: &Lines, - text_prov: impl TextLayoutProvider, - vline: VLine, - ) -> Option<(usize, RVLine)> { - find_vline_init_info_forward(lines, &text_prov, (VLine(0), 0), vline) - } - - fn fbvline_info( + fn fvline_info( lines: &Lines, text_prov: impl TextLayoutProvider, vline: VLine, ) -> Option<(usize, RVLine)> { - let last_vline = lines.last_vline(&text_prov); - let last_rvline = lines.last_rvline(&text_prov); - find_vline_init_info_rv_backward(lines, &text_prov, (last_vline, last_rvline), vline) + find_vline_init_info(lines, &text_prov, vline) } #[test] @@ -2340,15 +2173,10 @@ mod tests { let (text_prov, lines) = make_lines(&text, 50.0, false); assert_eq!( - ffvline_info(&lines, &text_prov, VLine(0)), - Some((0, RVLine::new(0, 0))) - ); - assert_eq!( - fbvline_info(&lines, &text_prov, VLine(0)), + fvline_info(&lines, &text_prov, VLine(0)), Some((0, RVLine::new(0, 0))) ); - assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(1)), None); // Test empty buffer with phantom text and no wrapping let text = Rope::from(""); @@ -2362,46 +2190,28 @@ mod tests { let (text_prov, lines) = make_lines_ph(&text, 20.0, false, ph); assert_eq!( - ffvline_info(&lines, &text_prov, VLine(0)), + fvline_info(&lines, &text_prov, VLine(0)), Some((0, RVLine::new(0, 0))) ); - assert_eq!( - fbvline_info(&lines, &text_prov, VLine(0)), - Some((0, RVLine::new(0, 0))) - ); - assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(1)), None); // Test empty buffer with phantom text and wrapping lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); assert_eq!( - ffvline_info(&lines, &text_prov, VLine(0)), - Some((0, RVLine::new(0, 0))) - ); - assert_eq!( - fbvline_info(&lines, &text_prov, VLine(0)), + fvline_info(&lines, &text_prov, VLine(0)), Some((0, RVLine::new(0, 0))) ); assert_eq!( - ffvline_info(&lines, &text_prov, VLine(1)), - Some((0, RVLine::new(0, 1))) - ); - assert_eq!( - fbvline_info(&lines, &text_prov, VLine(1)), + fvline_info(&lines, &text_prov, VLine(1)), Some((0, RVLine::new(0, 1))) ); assert_eq!( - ffvline_info(&lines, &text_prov, VLine(2)), - Some((0, RVLine::new(0, 2))) - ); - assert_eq!( - fbvline_info(&lines, &text_prov, VLine(2)), + fvline_info(&lines, &text_prov, VLine(2)), Some((0, RVLine::new(0, 2))) ); // Going outside bounds only ends up with None - assert_eq!(ffvline_info(&lines, &text_prov, VLine(3)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(3)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(3)), None); // The affinity would shift from the front/end of the phantom line // TODO: test affinity of logic behind clicking past the last vline? } @@ -2418,14 +2228,11 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( render_breaks(&text, &mut lines), @@ -2438,14 +2245,11 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( render_breaks(&text, &mut lines), @@ -2473,10 +2277,7 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } @@ -2487,10 +2288,7 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } @@ -2509,10 +2307,7 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } @@ -2532,26 +2327,15 @@ mod tests { // With text layouts, the phantom text is applied. // With a phantom text that takes up multiple lines, it does not affect the offsets // but it does affect the valid visual lines. - let info = ffvline_info(&lines, &text_prov, VLine(0)); - assert_eq!(info, Some((0, RVLine::new(0, 0)))); - let info = fbvline_info(&lines, &text_prov, VLine(0)); + let info = fvline_info(&lines, &text_prov, VLine(0)); assert_eq!(info, Some((0, RVLine::new(0, 0)))); - let info = ffvline_info(&lines, &text_prov, VLine(1)); - assert_eq!(info, Some((0, RVLine::new(0, 1)))); - let info = fbvline_info(&lines, &text_prov, VLine(1)); + let info = fvline_info(&lines, &text_prov, VLine(1)); assert_eq!(info, Some((0, RVLine::new(0, 1)))); for line in 2..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line - 1); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!( - info, - (line_offset, RVLine::new(line - 1, 0)), - "vline {}", - line - ); - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!( info, (line_offset, RVLine::new(line - 1, 0)), @@ -2563,14 +2347,7 @@ mod tests { // Then there's one extra vline due to the phantom text wrapping let line_offset = rope_text.offset_of_line(rope_text.last_line()); - let info = ffvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); - assert_eq!( - info, - Some((line_offset, RVLine::new(rope_text.last_line(), 0))), - "line {}", - rope_text.last_line() + 1, - ); - let info = fbvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); + let info = fvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); assert_eq!( info, Some((line_offset, RVLine::new(rope_text.last_line(), 0))), @@ -2592,7 +2369,7 @@ mod tests { // With no text layouts, phantom text isn't initialized so it has no affect. for line in 0..rope_text.num_lines() { - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); let line_offset = rope_text.offset_of_line(line); @@ -2618,22 +2395,15 @@ mod tests { for line in 0..3 { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } // ' end' - let info = ffvline_info(&lines, &text_prov, VLine(3)); - assert_eq!(info, Some((29, RVLine::new(2, 1)))); - let info = fbvline_info(&lines, &text_prov, VLine(3)); + let info = fvline_info(&lines, &text_prov, VLine(3)); assert_eq!(info, Some((29, RVLine::new(2, 1)))); - let info = ffvline_info(&lines, &text_prov, VLine(4)); - assert_eq!(info, Some((34, RVLine::new(3, 0)))); - let info = fbvline_info(&lines, &text_prov, VLine(4)); + let info = fvline_info(&lines, &text_prov, VLine(4)); assert_eq!(info, Some((34, RVLine::new(3, 0)))); } @@ -2651,15 +2421,11 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); } - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( render_breaks(&text, &mut lines), @@ -2693,14 +2459,7 @@ mod tests { assert_eq!(lines.last_rvline(&text_prov), RVLine::new(3, 0)); #[allow(clippy::needless_range_loop)] for line in 0..8 { - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!( - (info.0, info.1.line, info.1.line_index), - line_data[line], - "vline {}", - line - ); - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!( (info.0, info.1.line, info.1.line_index), line_data[line], @@ -2710,11 +2469,9 @@ mod tests { } // Directly out of bounds - assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None,); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None,); + assert_eq!(fvline_info(&lines, &text_prov, VLine(9)), None); - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( render_breaks(&text, &mut lines), @@ -2763,14 +2520,7 @@ mod tests { ]; #[allow(clippy::needless_range_loop)] for vline in 0..10 { - let info = ffvline_info(&lines, &text_prov, VLine(vline)).unwrap(); - assert_eq!( - (info.0, info.1.line, info.1.line_index), - line_data[vline], - "vline {}", - vline - ); - let info = fbvline_info(&lines, &text_prov, VLine(vline)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(vline)).unwrap(); assert_eq!( (info.0, info.1.line, info.1.line_index), line_data[vline], @@ -2822,15 +2572,7 @@ mod tests { for line in 0..rope_text.num_lines() { let line_offset = rope_text.offset_of_line(line); - let info = ffvline_info(&lines, &text_prov, VLine(line)); - assert_eq!( - info, - Some((line_offset, RVLine::new(line, 0))), - "line {}", - line - ); - - let info = fbvline_info(&lines, &text_prov, VLine(line)); + let info = fvline_info(&lines, &text_prov, VLine(line)); assert_eq!( info, Some((line_offset, RVLine::new(line, 0))), @@ -2839,8 +2581,7 @@ mod tests { ); } - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); assert_eq!( render_breaks(&text, &mut lines), @@ -2874,15 +2615,7 @@ mod tests { #[allow(clippy::needless_range_loop)] for line in 0..9 { - let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); - assert_eq!( - (info.0, info.1.line, info.1.line_index), - line_data[line], - "vline {}", - line - ); - - let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + let info = fvline_info(&lines, &text_prov, VLine(line)).unwrap(); assert_eq!( (info.0, info.1.line, info.1.line_index), line_data[line], @@ -2892,11 +2625,9 @@ mod tests { } // Directly out of bounds - assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(9)), None); - assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); - assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fvline_info(&lines, &text_prov, VLine(20)), None); // TODO: Currently the way we join phantom text and how cosmic wraps lines, // the phantom text will be joined with whatever the word next to it is - if there is no From be95cd7b738eb70b6797093e5460473c75d1b4f0 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Fri, 3 May 2024 08:18:39 -0500 Subject: [PATCH 17/21] Fix invalidation Uses a listener rather than invalidating only the editor doing the edit, for the obvious reason so as to invalidate all editors. (Though I would've liked to avoid using Listener) Also fixes & simplifies the invalidation calculation, adding tests to ensure correctness. --- src/views/editor/mod.rs | 6 ++ src/views/editor/text.rs | 14 ++- src/views/editor/text_document.rs | 19 ++-- src/views/editor/visual_line.rs | 174 +++++++++++++++++++++++++++--- 4 files changed, 193 insertions(+), 20 deletions(-) diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index ca4601d7..238fdcbc 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -1307,6 +1307,7 @@ fn create_view_effects(cx: Scope, ed: &Editor) { let ed2 = ed.clone(); let ed3 = ed.clone(); let ed4 = ed.clone(); + let ed5 = ed.clone(); // Reset cursor blinking whenever the cursor changes { @@ -1330,6 +1331,11 @@ fn create_view_effects(cx: Scope, ed: &Editor) { }); }; + let inval_lines_listener = ed.doc().inval_lines_listener(); + inval_lines_listener.listen_with(cx, move |inval_lines| { + ed5.lines.invalidate(&inval_lines); + }); + // Listen for layout events, currently only when a layout is created, and update screen // lines based on that ed3.lines.layout_event.listen_with(cx, move |val| { diff --git a/src/views/editor/text.rs b/src/views/editor/text.rs index 3f85884a..aaffb91c 100644 --- a/src/views/editor/text.rs +++ b/src/views/editor/text.rs @@ -10,7 +10,10 @@ use crate::{ }; use downcast_rs::{impl_downcast, Downcast}; use floem_editor_core::{ - buffer::rope_text::{RopeText, RopeTextVal}, + buffer::{ + rope_text::{RopeText, RopeTextVal}, + InvalLines, + }, command::EditCommand, cursor::Cursor, editor::EditType, @@ -30,6 +33,7 @@ use super::{ gutter::GutterClass, id::EditorId, layout::TextLayoutLine, + listener::Listener, normal_compute_screen_lines, phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, view::{ScreenLines, ScreenLinesBase}, @@ -95,6 +99,10 @@ pub trait Document: DocumentPhantom + Downcast { fn cache_rev(&self) -> RwSignal; + // TODO(minor): visual line doesn't really need to know the old rope that `InvalLines` passes + // around, should we just have a separate structure that doesn't have that field? + fn inval_lines_listener(&self) -> Listener; + /// Find the next/previous offset of the match of the given character. /// This is intended for use by the [Movement::NextUnmatched](floem_editor_core::movement::Movement::NextUnmatched) and /// [Movement::PreviousUnmatched](floem_editor_core::movement::Movement::PreviousUnmatched) commands. @@ -462,6 +470,10 @@ where self.doc.cache_rev() } + fn inval_lines_listener(&self) -> Listener { + self.doc.inval_lines_listener() + } + fn find_unmatched(&self, offset: usize, previous: bool, ch: char) -> usize { self.doc.find_unmatched(offset, previous, ch) } diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs index 23df9364..00177929 100644 --- a/src/views/editor/text_document.rs +++ b/src/views/editor/text_document.rs @@ -25,6 +25,7 @@ use super::{ actions::{handle_command_default, CommonAction}, command::{Command, CommandExecuted}, id::EditorId, + listener::Listener, phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, text::{Document, DocumentPhantom, PreeditData, SystemClipboard}, Editor, EditorStyle, @@ -67,6 +68,8 @@ pub struct TextDocument { pub placeholders: RwSignal>, + inval_lines_listener: Listener, + // (cmd: &Command, count: Option, modifiers: ModifierState) /// Ran before a command is executed. If it says that it executed the command, then handlers /// after it will not be called. @@ -99,6 +102,7 @@ impl TextDocument { preedit, keep_indent: Cell::new(true), auto_indent: Cell::new(false), + inval_lines_listener: Listener::new_empty(cx), placeholders, pre_command: Rc::new(RefCell::new(HashMap::new())), on_updates: Rc::new(RefCell::new(SmallVec::new())), @@ -109,24 +113,21 @@ impl TextDocument { self.buffer } - fn update_cache_rev(&self) { + pub fn update_cache_rev(&self) { self.cache_rev.try_update(|cache_rev| { *cache_rev += 1; }); } - fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLines)]) { + pub fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLines)]) { let on_updates = self.on_updates.borrow(); let data = OnUpdate { editor: ed, deltas }; for on_update in on_updates.iter() { on_update(data.clone()); } - // TODO: check what cases the editor might be `None`... - if let Some(ed) = ed { - for (_, _, inval_lines) in deltas { - ed.lines.invalidate(inval_lines); - } + for (_, _, inval_lines) in deltas { + self.inval_lines_listener().send(inval_lines.clone()); } } @@ -173,6 +174,10 @@ impl Document for TextDocument { self.cache_rev } + fn inval_lines_listener(&self) -> Listener { + self.inval_lines_listener + } + fn preedit(&self) -> PreeditData { self.preedit.clone() } diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index d2601867..b116f41b 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -126,7 +126,7 @@ impl RVLine { /// The cached text layouts. /// Starts at a specific `base_line`, and then grows from there. /// This is internally an array, so that newlines and moving the viewport up can be easily handled. -#[derive(Default)] +#[derive(Default, Clone)] pub struct Layouts { base_line: usize, layouts: Vec>>, @@ -197,11 +197,18 @@ impl Layouts { pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { let ib_start_line = start_line.max(self.base_line); let start_idx = self.idx(ib_start_line).unwrap(); + if start_idx >= self.layouts.len() { return; } - let end_idx = start_idx + inval_count; + let end_idx = if start_line >= self.base_line { + start_idx + inval_count + } else { + // If start_line + inval_count isn't within the range of the layouts then it'd just be 0 + let within_count = inval_count.saturating_sub(self.base_line - start_line); + start_idx + within_count + }; let ib_end_idx = end_idx.min(self.layouts.len()); for i in start_idx..ib_end_idx { @@ -217,18 +224,33 @@ impl Layouts { self.layouts .splice(ib_end_idx..ib_end_idx, std::iter::repeat(None).take(extra)); } else { - let remove = inval_count - new_count; - // But remove is not just the difference between inval count and and new count - // As we cut off the end of the interval if it went past the end of the layouts, + // How many (invalidated) line entries should be removed. + // (Since all of the lines in the inval lines area are `None` now, it doesn't matter if + // they were some other line number originally if we're draining them out) + let mut to_remove = inval_count; + let mut to_keep = new_count; + let oob_start = ib_start_line - start_line; - let oob_end = ib_end_idx - end_idx.min(self.layouts.len()); - let oob_remove = oob_start + oob_end; - let remove = remove - oob_remove; + // Shift the base line backwards by the amount outside the start + // This allows us to not bother with removing entries from the array in some cases + { + let oob_start_remove = oob_start.min(to_remove); + + self.base_line -= oob_start_remove; + to_remove = to_remove.saturating_sub(oob_start_remove); + to_keep = to_keep.saturating_sub(oob_start_remove); + } + + if to_remove == 0 { + // There is nothing more to remove + return; + } + + let remove_start_idx = start_idx + to_keep; + let remove_end_idx = (start_idx + to_remove).min(self.layouts.len()); - // Since we set all the layouts in the interval to None, we can just do the simpler - // task of removing from the start. - self.layouts.drain(start_idx..start_idx + remove); + drop(self.layouts.drain(remove_start_idx..remove_end_idx)); } } } @@ -1935,7 +1957,8 @@ mod tests { }; use super::{ - find_vline_init_info, ConfigId, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine, + find_vline_init_info, ConfigId, Layouts, Lines, RVLine, ResolvedWrap, TextLayoutProvider, + VLine, }; /// For most of the logic we standardize on a specific font size. @@ -3343,4 +3366,131 @@ mod tests { "simple multiline (CRLF)", ); } + + #[test] + fn layout_cache() { + let random_layout = |text: &str| { + let mut text_layout = TextLayout::new(); + text_layout.set_text(text, AttrsList::new(Attrs::new())); + + Arc::new(TextLayoutLine { + extra_style: Vec::new(), + text: text_layout, + whitespaces: None, + indent: 0., + phantom_text: PhantomTextLine::default(), + }) + }; + let mut layouts = Layouts::default(); + + assert_eq!(layouts.base_line, 0); + assert!(layouts.layouts.is_empty()); + + layouts.insert(0, random_layout("abc")); + assert_eq!(layouts.base_line, 0); + assert_eq!(layouts.layouts.len(), 1); + + layouts.insert(1, random_layout("def")); + assert_eq!(layouts.base_line, 0); + assert_eq!(layouts.layouts.len(), 2); + + layouts.insert(10, random_layout("ghi")); + assert_eq!(layouts.base_line, 0); + assert_eq!(layouts.layouts.len(), 11); + + let mut layouts = Layouts::default(); + layouts.insert(10, random_layout("ghi")); + assert_eq!(layouts.base_line, 10); + assert_eq!(layouts.layouts.len(), 1); + + layouts.insert(8, random_layout("jkl")); + assert_eq!(layouts.base_line, 8); + assert_eq!(layouts.layouts.len(), 3); + + layouts.insert(5, random_layout("mno")); + assert_eq!(layouts.base_line, 5); + assert_eq!(layouts.layouts.len(), 6); + + assert!(layouts.get(0).is_none()); + assert!(layouts.get(5).is_some()); + assert!(layouts.get(8).is_some()); + assert!(layouts.get(10).is_some()); + assert!(layouts.get(11).is_none()); + + let mut layouts2 = layouts.clone(); + layouts2.invalidate(0, 1, 1); + assert!(layouts2.get(0).is_none()); + assert!(layouts2.get(5).is_some()); + assert!(layouts2.get(8).is_some()); + assert!(layouts2.get(10).is_some()); + assert!(layouts2.get(11).is_none()); + + let mut layouts2 = layouts.clone(); + layouts2.invalidate(5, 1, 1); + assert!(layouts2.get(0).is_none()); + assert!(layouts2.get(5).is_none()); + assert!(layouts2.get(8).is_some()); + assert!(layouts2.get(10).is_some()); + assert!(layouts2.get(11).is_none()); + + layouts.invalidate(0, 6, 6); + assert!(layouts.get(5).is_none()); + assert!(layouts.get(8).is_some()); + assert!(layouts.get(10).is_some()); + assert!(layouts.get(11).is_none()); + + let mut layouts = Layouts::default(); + for i in 0..10 { + let text = format!("{}", i); + layouts.insert(i, random_layout(&text)); + } + + assert_eq!(layouts.base_line, 0); + assert_eq!(layouts.layouts.len(), 10); + + layouts.invalidate(0, 10, 1); + assert!(layouts.get(0).is_none()); + assert_eq!(layouts.len(), 1); + + let mut layouts = Layouts::default(); + for i in 0..10 { + let text = format!("{}", i); + layouts.insert(i, random_layout(&text)); + } + + layouts.invalidate(5, 800, 1); + assert!(layouts.get(0).is_some()); + assert!(layouts.get(1).is_some()); + assert!(layouts.get(2).is_some()); + assert!(layouts.get(3).is_some()); + assert!(layouts.get(4).is_some()); + assert_eq!(layouts.len(), 6); + + let mut layouts = Layouts::default(); + for i in 5..10 { + let text = format!("{}", i); + layouts.insert(i, random_layout(&text)); + } + + assert_eq!(layouts.base_line, 5); + + layouts.invalidate(0, 7, 1); + assert_eq!(layouts.base_line, 0); + assert!(layouts.get(0).is_some()); // was line 7 + assert!(layouts.get(1).is_some()); // was line 8 + assert!(layouts.get(2).is_some()); // was line 9 + assert!(layouts.get(3).is_none()); + assert!(layouts.get(4).is_none()); + assert_eq!(layouts.len(), 3); + + let mut layouts = Layouts::default(); + for i in 0..10 { + let text = format!("{}", i); + layouts.insert(i, random_layout(&text)); + } + + layouts.invalidate(0, 800, 1); + assert!(layouts.get(0).is_none()); + assert_eq!(layouts.len(), 1); + } } From 45ebdbddcd2d10f99244ff815f2efa02fcac5270 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Fri, 3 May 2024 08:43:27 -0500 Subject: [PATCH 18/21] InvalCount without Rope --- editor-core/src/buffer/mod.rs | 45 ++++++++++++++++++++++------- editor-core/src/editor.rs | 24 ++++++++-------- src/views/editor/mod.rs | 2 +- src/views/editor/text_document.rs | 8 +++--- src/views/editor/visual_line.rs | 47 ++++++++++++++++--------------- 5 files changed, 76 insertions(+), 50 deletions(-) diff --git a/editor-core/src/buffer/mod.rs b/editor-core/src/buffer/mod.rs index c1c94933..ec0191ed 100644 --- a/editor-core/src/buffer/mod.rs +++ b/editor-core/src/buffer/mod.rs @@ -63,13 +63,38 @@ struct Revision { cursor_after: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct InvalLines { pub start_line: usize, pub inval_count: usize, pub new_count: usize, +} +impl InvalLines { + pub fn new(start_line: usize, inval_count: usize, new_count: usize) -> Self { + Self { + start_line, + inval_count, + new_count, + } + } +} + +#[derive(Debug, Clone)] +pub struct InvalLinesR { + pub start_line: usize, + pub inval_count: usize, + pub new_count: usize, pub old_text: Rope, } +impl Into for InvalLinesR { + fn into(self) -> InvalLines { + InvalLines { + start_line: self.start_line, + inval_count: self.inval_count, + new_count: self.new_count, + } + } +} #[derive(Clone)] pub struct Buffer { @@ -214,7 +239,7 @@ impl Buffer { self.set_pristine(); } - pub fn reload(&mut self, content: Rope, set_pristine: bool) -> (Rope, RopeDelta, InvalLines) { + pub fn reload(&mut self, content: Rope, set_pristine: bool) -> (Rope, RopeDelta, InvalLinesR) { // Determine the line ending of the new text let line_ending = LineEndingDetermination::determine(&content); self.line_ending = line_ending.unwrap_or(self.line_ending); @@ -262,7 +287,7 @@ impl Buffer { &mut self, edits: I, edit_type: EditType, - ) -> (Rope, RopeDelta, InvalLines) + ) -> (Rope, RopeDelta, InvalLinesR) where I: IntoIterator, E: Borrow<(S, &'a str)>, @@ -299,7 +324,7 @@ impl Buffer { self.add_delta(delta) } - pub fn normalize_line_endings(&mut self) -> Option<(Rope, RopeDelta, InvalLines)> { + pub fn normalize_line_endings(&mut self) -> Option<(Rope, RopeDelta, InvalLinesR)> { let Some(delta) = self.line_ending.normalize_delta(&self.text) else { // There were no changes needed return None; @@ -310,7 +335,7 @@ impl Buffer { // TODO: don't clone the delta and return it, if the caller needs it then they can clone it /// Note: the delta's line-endings should be normalized. - fn add_delta(&mut self, delta: RopeDelta) -> (Rope, RopeDelta, InvalLines) { + fn add_delta(&mut self, delta: RopeDelta) -> (Rope, RopeDelta, InvalLinesR) { let text = self.text.clone(); let undo_group = self.calculate_undo_group(); @@ -337,7 +362,7 @@ impl Buffer { new_text: Rope, new_tombstones: Rope, new_deletes_from_union: Subset, - ) -> InvalLines { + ) -> InvalLinesR { self.rev_counter += 1; let (iv, newlen) = delta.summary(); @@ -354,7 +379,7 @@ impl Buffer { let old_hard_count = old_logical_end_line - logical_start_line; let new_hard_count = new_logical_end_line - logical_start_line; - InvalLines { + InvalLinesR { start_line: logical_start_line, inval_count: old_hard_count, new_count: new_hard_count, @@ -588,7 +613,7 @@ impl Buffer { ) -> ( Rope, RopeDelta, - InvalLines, + InvalLinesR, Option, Option, ) { @@ -622,7 +647,7 @@ impl Buffer { (text, delta, inval_lines, cursor_before, cursor_after) } - pub fn do_undo(&mut self) -> Option<(Rope, RopeDelta, InvalLines, Option)> { + pub fn do_undo(&mut self) -> Option<(Rope, RopeDelta, InvalLinesR, Option)> { if self.cur_undo <= 1 { return None; } @@ -636,7 +661,7 @@ impl Buffer { Some((text, delta, inval_lines, cursor_before)) } - pub fn do_redo(&mut self) -> Option<(Rope, RopeDelta, InvalLines, Option)> { + pub fn do_redo(&mut self) -> Option<(Rope, RopeDelta, InvalLinesR, Option)> { if self.cur_undo >= self.live_undos.len() { return None; } diff --git a/editor-core/src/editor.rs b/editor-core/src/editor.rs index ab98179e..ce037afa 100644 --- a/editor-core/src/editor.rs +++ b/editor-core/src/editor.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use lapce_xi_rope::{DeltaElement, Rope, RopeDelta}; use crate::{ - buffer::{rope_text::RopeText, Buffer, InvalLines}, + buffer::{rope_text::RopeText, Buffer, InvalLinesR}, command::EditCommand, cursor::{get_first_selection_after, Cursor, CursorMode}, mode::{Mode, MotionMode, VisualMode}, @@ -93,7 +93,7 @@ impl Action { prev_unmatched: &dyn Fn(&Buffer, char, usize) -> Option, auto_closing_matching_pairs: bool, auto_surround: bool, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { let mut deltas = Vec::new(); if let CursorMode::Insert(selection) = &cursor.mode { if s.chars().count() != 1 { @@ -323,7 +323,7 @@ impl Action { selection: Selection, keep_indent: bool, auto_indent: bool, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { let mut edits = Vec::with_capacity(selection.regions().len()); let mut extra_edits = Vec::new(); let mut shift = 0i32; @@ -411,7 +411,7 @@ impl Action { range: Range, is_vertical: bool, register: &mut Register, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { let mut deltas = Vec::new(); match motion_mode { MotionMode::Delete { .. } => { @@ -471,7 +471,7 @@ impl Action { selection: &Selection, content: &str, mode: VisualMode, - ) -> (Rope, RopeDelta, InvalLines) { + ) -> (Rope, RopeDelta, InvalLinesR) { if selection.len() > 1 { let line_ends: Vec<_> = content.match_indices('\n').map(|(idx, _)| idx).collect(); @@ -551,7 +551,7 @@ impl Action { cursor: &mut Cursor, buffer: &mut Buffer, data: &RegisterData, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { let mut deltas = Vec::new(); match data.mode { VisualMode::Normal => { @@ -638,7 +638,7 @@ impl Action { deltas } - fn do_indent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLines) { + fn do_indent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLinesR) { let indent = buffer.indent_unit(); let mut edits = Vec::new(); @@ -667,7 +667,7 @@ impl Action { buffer.edit(&edits, EditType::Indent) } - fn do_outdent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLines) { + fn do_outdent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLinesR) { let indent = buffer.indent_unit(); let mut edits = Vec::new(); @@ -701,7 +701,7 @@ impl Action { cursor: &mut Cursor, buffer: &mut Buffer, direction: DuplicateDirection, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { // TODO other modes let selection = match cursor.mode { CursorMode::Insert(ref mut sel) => sel, @@ -757,7 +757,7 @@ impl Action { keep_indent, auto_indent, }: EditConf, - ) -> Vec<(Rope, RopeDelta, InvalLines)> { + ) -> Vec<(Rope, RopeDelta, InvalLinesR)> { use crate::command::EditCommand::*; match cmd { MoveLineUp => { @@ -1478,9 +1478,9 @@ fn apply_undo_redo( modal: bool, text: Rope, delta: RopeDelta, - inval_lines: InvalLines, + inval_lines: InvalLinesR, cursor_mode: Option, -) -> Vec<(Rope, RopeDelta, InvalLines)> { +) -> Vec<(Rope, RopeDelta, InvalLinesR)> { if let Some(cursor_mode) = cursor_mode { cursor.mode = if modal { CursorMode::Normal(cursor_mode.offset()) diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index 238fdcbc..b9d10067 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -1333,7 +1333,7 @@ fn create_view_effects(cx: Scope, ed: &Editor) { let inval_lines_listener = ed.doc().inval_lines_listener(); inval_lines_listener.listen_with(cx, move |inval_lines| { - ed5.lines.invalidate(&inval_lines); + ed5.lines.invalidate(inval_lines); }); // Listen for layout events, currently only when a layout is created, and update screen diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs index 00177929..feab8344 100644 --- a/src/views/editor/text_document.rs +++ b/src/views/editor/text_document.rs @@ -6,7 +6,7 @@ use std::{ }; use floem_editor_core::{ - buffer::{rope_text::RopeText, Buffer, InvalLines}, + buffer::{rope_text::RopeText, Buffer, InvalLines, InvalLinesR}, command::EditCommand, cursor::Cursor, editor::{Action, EditConf, EditType}, @@ -45,7 +45,7 @@ type OnUpdateFn = Box; pub struct OnUpdate<'a> { /// Optional because the document can be edited from outside any editor views pub editor: Option<&'a Editor>, - deltas: &'a [(Rope, RopeDelta, InvalLines)], + deltas: &'a [(Rope, RopeDelta, InvalLinesR)], } impl<'a> OnUpdate<'a> { pub fn deltas(&self) -> impl Iterator { @@ -119,7 +119,7 @@ impl TextDocument { }); } - pub fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLines)]) { + pub fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLinesR)]) { let on_updates = self.on_updates.borrow(); let data = OnUpdate { editor: ed, deltas }; for on_update in on_updates.iter() { @@ -127,7 +127,7 @@ impl TextDocument { } for (_, _, inval_lines) in deltas { - self.inval_lines_listener().send(inval_lines.clone()); + self.inval_lines_listener().send(inval_lines.clone().into()); } } diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index b116f41b..344fb173 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -194,7 +194,14 @@ impl Layouts { /// Invalidates the layouts at the given `start_line` for `inval_count` lines. /// `new_count` is used to know whether to insert new line entries or to remove them, such as /// for a newline. - pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { + pub fn invalidate( + &mut self, + InvalLines { + start_line, + inval_count, + new_count, + }: InvalLines, + ) { let ib_start_line = start_line.max(self.base_line); let start_idx = self.idx(ib_start_line).unwrap(); @@ -331,8 +338,8 @@ impl TextLayoutCache { }) } - pub fn invalidate(&mut self, start_line: usize, inval_count: usize, new_count: usize) { - self.layouts.invalidate(start_line, inval_count, new_count); + pub fn invalidate(&mut self, inval: InvalLines) { + self.layouts.invalidate(inval); } } @@ -996,21 +1003,12 @@ impl Lines { self.last_vline.set(None); } - pub fn invalidate(&self, inval_lines: &InvalLines) { - let InvalLines { - start_line, - inval_count, - new_count, - .. - } = *inval_lines; - - if inval_count == 0 { + pub fn invalidate(&self, inval: InvalLines) { + if inval.inval_count == 0 { return; } - self.text_layouts - .borrow_mut() - .invalidate(start_line, inval_count, new_count); + self.text_layouts.borrow_mut().invalidate(inval); } } @@ -1942,7 +1940,10 @@ mod tests { use std::{borrow::Cow, collections::HashMap, sync::Arc}; use floem_editor_core::{ - buffer::rope_text::{RopeText, RopeTextRef, RopeTextVal}, + buffer::{ + rope_text::{RopeText, RopeTextRef, RopeTextVal}, + InvalLines, + }, cursor::CursorAffinity, }; use floem_reactive::Scope; @@ -3418,7 +3419,7 @@ mod tests { assert!(layouts.get(11).is_none()); let mut layouts2 = layouts.clone(); - layouts2.invalidate(0, 1, 1); + layouts2.invalidate(InvalLines::new(0, 1, 1)); assert!(layouts2.get(0).is_none()); assert!(layouts2.get(5).is_some()); assert!(layouts2.get(8).is_some()); @@ -3426,14 +3427,14 @@ mod tests { assert!(layouts2.get(11).is_none()); let mut layouts2 = layouts.clone(); - layouts2.invalidate(5, 1, 1); + layouts2.invalidate(InvalLines::new(5, 1, 1)); assert!(layouts2.get(0).is_none()); assert!(layouts2.get(5).is_none()); assert!(layouts2.get(8).is_some()); assert!(layouts2.get(10).is_some()); assert!(layouts2.get(11).is_none()); - layouts.invalidate(0, 6, 6); + layouts.invalidate(InvalLines::new(0, 6, 6)); assert!(layouts.get(5).is_none()); assert!(layouts.get(8).is_some()); assert!(layouts.get(10).is_some()); @@ -3448,7 +3449,7 @@ mod tests { assert_eq!(layouts.base_line, 0); assert_eq!(layouts.layouts.len(), 10); - layouts.invalidate(0, 10, 1); + layouts.invalidate(InvalLines::new(0, 10, 1)); assert!(layouts.get(0).is_none()); assert_eq!(layouts.len(), 1); @@ -3458,7 +3459,7 @@ mod tests { layouts.insert(i, random_layout(&text)); } - layouts.invalidate(5, 800, 1); + layouts.invalidate(InvalLines::new(5, 800, 1)); assert!(layouts.get(0).is_some()); assert!(layouts.get(1).is_some()); assert!(layouts.get(2).is_some()); @@ -3474,7 +3475,7 @@ mod tests { assert_eq!(layouts.base_line, 5); - layouts.invalidate(0, 7, 1); + layouts.invalidate(InvalLines::new(0, 7, 1)); assert_eq!(layouts.base_line, 0); assert!(layouts.get(0).is_some()); // was line 7 assert!(layouts.get(1).is_some()); // was line 8 @@ -3489,7 +3490,7 @@ mod tests { layouts.insert(i, random_layout(&text)); } - layouts.invalidate(0, 800, 1); + layouts.invalidate(InvalLines::new(0, 800, 1)); assert!(layouts.get(0).is_none()); assert_eq!(layouts.len(), 1); } From a8ca2eaf76033fe0c95633410458795bbae0c5d1 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Fri, 3 May 2024 11:19:46 -0500 Subject: [PATCH 19/21] Remove cache_rev Also does some minor changes to Document --- editor-core/src/buffer/mod.rs | 8 +++ examples/editor/src/main.rs | 1 + examples/syntax-editor/src/main.rs | 1 + src/views/editor/mod.rs | 38 +++++++------- src/views/editor/text.rs | 28 ++++++---- src/views/editor/text_document.rs | 30 +++-------- src/views/editor/visual_line.rs | 83 +++++++++++------------------- 7 files changed, 83 insertions(+), 106 deletions(-) diff --git a/editor-core/src/buffer/mod.rs b/editor-core/src/buffer/mod.rs index ec0191ed..eea2acb0 100644 --- a/editor-core/src/buffer/mod.rs +++ b/editor-core/src/buffer/mod.rs @@ -77,6 +77,14 @@ impl InvalLines { new_count, } } + + pub fn single(start_line: usize) -> Self { + Self { + start_line, + inval_count: 1, + new_count: 1, + } + } } #[derive(Debug, Clone)] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index fc0e5232..6a8df5d3 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -52,6 +52,7 @@ fn app_view() -> impl IntoView { stack(( button(|| "Clear").on_click_stop(move |_| { doc.edit_single( + None, Selection::region(0, doc.text().len()), "", EditType::DeleteSelection, diff --git a/examples/syntax-editor/src/main.rs b/examples/syntax-editor/src/main.rs index 41f584df..e26310e0 100644 --- a/examples/syntax-editor/src/main.rs +++ b/examples/syntax-editor/src/main.rs @@ -230,6 +230,7 @@ mod tests { stack(( button(|| "Clear").on_click_stop(move |_| { doc.edit_single( + None, Selection::region(0, doc.text().len()), "", EditType::DeleteSelection, diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index b9d10067..ea815b5f 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -1,4 +1,4 @@ -use core::indent::IndentStyle; +use core::{buffer::InvalLines, indent::IndentStyle}; use std::{cell::Cell, cmp::Ordering, collections::HashMap, rc::Rc, sync::Arc, time::Duration}; use crate::{ @@ -322,7 +322,7 @@ impl Editor { // Get rid of all the effects self.effects_cx.get().dispose(); - self.lines.clear(0, None); + self.lines.clear(None); self.doc.set(doc); if let Some(styling) = styling { self.style.set(styling); @@ -342,7 +342,7 @@ impl Editor { // Get rid of all the effects self.effects_cx.get().dispose(); - self.lines.clear(0, None); + self.lines.clear(None); self.style.set(styling); @@ -426,10 +426,10 @@ impl Editor { cursor, offset, })); - - self.doc().cache_rev().update(|cache_rev| { - *cache_rev += 1; - }); + let line = self.rope_text().line_of_offset(offset); + self.doc() + .inval_lines_listener() + .send(InvalLines::single(line)); }); } @@ -440,10 +440,17 @@ impl Editor { } batch(|| { + let offset = preedit + .preedit + .with_untracked(|p| p.as_ref().map(|p| p.offset)); preedit.preedit.set(None); - self.doc().cache_rev().update(|cache_rev| { - *cache_rev += 1; - }); + + if let Some(offset) = offset { + let line = self.rope_text().line_of_offset(offset); + self.doc() + .inval_lines_listener() + .send(InvalLines::single(line)); + } }); } @@ -1082,15 +1089,12 @@ impl Editor { } pub fn text_layout_trigger(&self, line: usize, trigger: bool) -> Arc { - let cache_rev = self.doc().cache_rev().get_untracked(); self.lines - .get_init_text_layout(cache_rev, self.config_id(), self, line, trigger) + .get_init_text_layout(self.config_id(), self, line, trigger) } fn try_get_text_layout(&self, line: usize) -> Option> { - let cache_rev = self.doc().cache_rev().get_untracked(); - self.lines - .try_get_text_layout(cache_rev, self.config_id(), line) + self.lines.try_get_text_layout(self.config_id(), line) } /// Create rendable whitespace layout by creating a new text layout @@ -1426,9 +1430,6 @@ pub fn normal_compute_screen_lines( let min_vline = VLine((y0 / line_height as f64).floor() as usize); let max_vline = VLine((y1 / line_height as f64).ceil() as usize); - let cache_rev = editor.doc.get().cache_rev().get(); - editor.lines.check_cache_rev(cache_rev); - let min_info = editor.iter_vlines(false, min_vline).next(); let mut rvlines = Vec::new(); @@ -1449,7 +1450,6 @@ pub fn normal_compute_screen_lines( let iter = lines .iter_rvlines_init( editor.text_prov(), - cache_rev, editor.config_id(), min_info.rvline, false, diff --git a/src/views/editor/text.rs b/src/views/editor/text.rs index aaffb91c..9504966e 100644 --- a/src/views/editor/text.rs +++ b/src/views/editor/text.rs @@ -97,8 +97,6 @@ pub trait Document: DocumentPhantom + Downcast { RopeTextVal::new(self.text()) } - fn cache_rev(&self) -> RwSignal; - // TODO(minor): visual line doesn't really need to know the old rope that `InvalLines` passes // around, should we just have a separate structure that doesn't have that field? fn inval_lines_listener(&self) -> Listener; @@ -179,7 +177,13 @@ pub trait Document: DocumentPhantom + Downcast { fn receive_char(&self, ed: &Editor, c: &str); /// Perform a single edit. - fn edit_single(&self, ed: &Editor, selection: Selection, content: &str, edit_type: EditType) { + fn edit_single( + &self, + ed: Option<&Editor>, + selection: Selection, + content: &str, + edit_type: EditType, + ) { let mut iter = std::iter::once((selection, content)); self.edit(ed, &mut iter, edit_type); } @@ -195,13 +199,13 @@ pub trait Document: DocumentPhantom + Downcast { /// editor, /// button(|| "Append 'Hello'").on_click_stop(move |_| { /// let text = doc.text(); - /// doc.edit_single(Selection::caret(text.len()), "Hello", EditType::InsertChars); + /// doc.edit_single(None, Selection::caret(text.len()), "Hello", EditType::InsertChars); /// }) /// )) /// ``` fn edit( &self, - ed: &Editor, + ed: Option<&Editor>, iter: &mut dyn Iterator, edit_type: EditType, ); @@ -466,10 +470,6 @@ where self.doc.rope_text() } - fn cache_rev(&self) -> RwSignal { - self.doc.cache_rev() - } - fn inval_lines_listener(&self) -> Listener { self.doc.inval_lines_listener() } @@ -516,13 +516,19 @@ where self.doc.receive_char(ed, c) } - fn edit_single(&self, ed: &Editor, selection: Selection, content: &str, edit_type: EditType) { + fn edit_single( + &self, + ed: Option<&Editor>, + selection: Selection, + content: &str, + edit_type: EditType, + ) { self.doc.edit_single(ed, selection, content, edit_type) } fn edit( &self, - ed: &Editor, + ed: Option<&Editor>, iter: &mut dyn Iterator, edit_type: EditType, ) { diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs index feab8344..927afb51 100644 --- a/src/views/editor/text_document.rs +++ b/src/views/editor/text_document.rs @@ -58,7 +58,6 @@ impl<'a> OnUpdate<'a> { #[derive(Clone)] pub struct TextDocument { buffer: RwSignal, - cache_rev: RwSignal, preedit: PreeditData, /// Whether to keep the indent of the previous line when inserting a new line @@ -84,25 +83,22 @@ impl TextDocument { let preedit = PreeditData { preedit: cx.create_rw_signal(None), }; - let cache_rev = cx.create_rw_signal(0); + let inval_lines_listener = Listener::new_empty(cx); let placeholders = cx.create_rw_signal(HashMap::new()); - // Whenever the placeholders change, update the cache rev + // Whenever the placeholders change, invalidate the first line create_effect(move |_| { placeholders.track(); - cache_rev.try_update(|cache_rev| { - *cache_rev += 1; - }); + inval_lines_listener.send(InvalLines::single(0)); }); TextDocument { buffer: cx.create_rw_signal(buffer), - cache_rev, preedit, keep_indent: Cell::new(true), auto_indent: Cell::new(false), - inval_lines_listener: Listener::new_empty(cx), + inval_lines_listener, placeholders, pre_command: Rc::new(RefCell::new(HashMap::new())), on_updates: Rc::new(RefCell::new(SmallVec::new())), @@ -113,12 +109,6 @@ impl TextDocument { self.buffer } - pub fn update_cache_rev(&self) { - self.cache_rev.try_update(|cache_rev| { - *cache_rev += 1; - }); - } - pub fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLinesR)]) { let on_updates = self.on_updates.borrow(); let data = OnUpdate { editor: ed, deltas }; @@ -170,10 +160,6 @@ impl Document for TextDocument { self.buffer.with_untracked(|buffer| buffer.text().clone()) } - fn cache_rev(&self) -> RwSignal { - self.cache_rev - } - fn inval_lines_listener(&self) -> Listener { self.inval_lines_listener } @@ -238,8 +224,6 @@ impl Document for TextDocument { buffer.set_cursor_before(old_cursor_mode); buffer.set_cursor_after(cursor.mode.clone()); }); - // TODO: line specific invalidation - self.update_cache_rev(); self.on_update(Some(ed), &deltas); } ed.cursor.set(cursor); @@ -248,7 +232,7 @@ impl Document for TextDocument { fn edit( &self, - ed: &Editor, + ed: Option<&Editor>, iter: &mut dyn Iterator, edit_type: EditType, ) { @@ -258,8 +242,7 @@ impl Document for TextDocument { let deltas = deltas.map(|x| [x]); let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]); - self.update_cache_rev(); - self.on_update(Some(ed), deltas); + self.on_update(ed, deltas); } } impl DocumentPhantom for TextDocument { @@ -371,7 +354,6 @@ impl CommonAction for TextDocument { buffer.set_cursor_after(cursor.mode.clone()); }); - self.update_cache_rev(); self.on_update(Some(ed), &deltas); } diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 344fb173..77f2b8c6 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -281,19 +281,16 @@ pub struct TextLayoutCache { /// The id of the last config so that we can clear when the config changes /// the first is the styling id and the second is an id for changes from Floem style config_id: ConfigId, - /// The most recent cache revision of the document. - cache_rev: u64, pub layouts: Layouts, /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar pub max_width: f64, } impl TextLayoutCache { - pub fn clear(&mut self, cache_rev: u64, config_id: Option) { + pub fn clear(&mut self, config_id: Option) { self.layouts.clear(); if let Some(config_id) = config_id { self.config_id = config_id; } - self.cache_rev = cache_rev; self.max_width = 0.0; } @@ -551,13 +548,12 @@ impl Lines { /// cache. pub fn get_init_text_layout( &self, - cache_rev: u64, config_id: ConfigId, text_prov: impl TextLayoutProvider, line: usize, trigger: bool, ) -> Arc { - self.check_cache(cache_rev, config_id); + self.check_cache(config_id); get_init_text_layout( &self.text_layouts, @@ -575,11 +571,10 @@ impl Lines { /// cache. pub fn try_get_text_layout( &self, - cache_rev: u64, config_id: ConfigId, line: usize, ) -> Option> { - self.check_cache(cache_rev, config_id); + self.check_cache(config_id); self.text_layouts.borrow().layouts.get(line).cloned() } @@ -590,14 +585,13 @@ impl Lines { /// the [`LayoutEvent::CreatedLayout`] event. pub fn init_line_interval( &self, - cache_rev: u64, config_id: ConfigId, text_prov: &impl TextLayoutProvider, lines: impl Iterator, trigger: bool, ) { for line in lines { - self.get_init_text_layout(cache_rev, config_id, text_prov, line, trigger); + self.get_init_text_layout(config_id, text_prov, line, trigger); } } @@ -608,14 +602,13 @@ impl Lines { /// the [`LayoutEvent::CreatedLayout`] event. pub fn init_all( &self, - cache_rev: u64, config_id: ConfigId, text_prov: &impl TextLayoutProvider, trigger: bool, ) { let text = text_prov.text(); let last_line = text.line_of_offset(text.len()); - self.init_line_interval(cache_rev, config_id, text_prov, 0..=last_line, trigger); + self.init_line_interval(config_id, text_prov, 0..=last_line, trigger); } /// Iterator over [`VLineInfo`]s, starting at `start_line`. @@ -674,17 +667,16 @@ impl Lines { pub fn iter_vlines_init( &self, text_prov: impl TextLayoutProvider + Clone, - cache_rev: u64, config_id: ConfigId, start: VLine, trigger: bool, ) -> impl Iterator { - self.check_cache(cache_rev, config_id); + self.check_cache(config_id); if start <= self.last_vline(&text_prov) { // We initialize the text layout for the line that start line is for let (_, rvline) = find_vline_init_info(self, &text_prov, start).unwrap(); - self.get_init_text_layout(cache_rev, config_id, &text_prov, rvline.line, trigger); + self.get_init_text_layout(config_id, &text_prov, rvline.line, trigger); // If the start line was past the last vline then we don't need to initialize anything // since it won't get anything. } @@ -727,13 +719,12 @@ impl Lines { pub fn iter_vlines_init_over( &self, text_prov: impl TextLayoutProvider + Clone, - cache_rev: u64, config_id: ConfigId, start: VLine, end: VLine, trigger: bool, ) -> impl Iterator { - self.iter_vlines_init(text_prov, cache_rev, config_id, start, trigger) + self.iter_vlines_init(text_prov, config_id, start, trigger) .take_while(move |info| info.vline < end) } @@ -746,16 +737,15 @@ impl Lines { pub fn iter_rvlines_init( &self, text_prov: impl TextLayoutProvider + Clone, - cache_rev: u64, config_id: ConfigId, start: RVLine, trigger: bool, ) -> impl Iterator> { - self.check_cache(cache_rev, config_id); + self.check_cache(config_id); if start.line <= text_prov.rope_text().last_line() { // Initialize the text layout for the line that start line is for - self.get_init_text_layout(cache_rev, config_id, &text_prov, start.line, trigger); + self.get_init_text_layout(config_id, &text_prov, start.line, trigger); } let text_layouts = self.text_layouts.clone(); @@ -969,31 +959,20 @@ impl Lines { } /// Check whether the cache rev or config id has changed, clearing the cache if it has. - pub fn check_cache(&self, cache_rev: u64, config_id: ConfigId) { - let (prev_cache_rev, prev_config_id) = { + pub fn check_cache(&self, config_id: ConfigId) { + let prev_config_id = { let l = self.text_layouts.borrow(); - (l.cache_rev, l.config_id) + l.config_id }; - // if cache_rev != prev_cache_rev || config_id != prev_config_id { - // self.clear(cache_rev, Some(config_id)); - // } if config_id != prev_config_id { - self.clear(cache_rev, Some(config_id)); + self.clear(Some(config_id)); } } - /// Check whether the text layout cache revision is different. - /// Clears the layouts and updates the cache rev if it was different. - pub fn check_cache_rev(&self, cache_rev: u64) { - // if cache_rev != self.text_layouts.borrow().cache_rev { - // self.clear(cache_rev, None); - // } - } - /// Clear the text layouts with a given cache revision - pub fn clear(&self, cache_rev: u64, config_id: Option) { - self.text_layouts.borrow_mut().clear(cache_rev, config_id); + pub fn clear(&self, config_id: Option) { + self.text_layouts.borrow_mut().clear(config_id); self.last_vline.set(None); } @@ -2107,7 +2086,7 @@ mod tests { if init { let config_id = 0; let floem_style_id = 0; - lines.init_all(0, ConfigId::new(config_id, floem_style_id), &text, true); + lines.init_all(ConfigId::new(config_id, floem_style_id), &text, true); } (text, lines) @@ -2220,7 +2199,7 @@ mod tests { assert_eq!(fvline_info(&lines, &text_prov, VLine(1)), None); // Test empty buffer with phantom text and wrapping - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); assert_eq!( fvline_info(&lines, &text_prov, VLine(0)), @@ -2263,7 +2242,7 @@ mod tests { ["hello", "world toast and jam", "the end", "hi"] ); - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); // Assert that even with text layouts, if it has no wrapping applied (because the width is large in this case) and no phantom text then it produces the same offsets as before. for line in 0..rope_text.num_lines() { @@ -2305,7 +2284,7 @@ mod tests { assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); // With text layouts, the phantom text is applied. // But with a single line of phantom text, it doesn't affect the offsets. @@ -2335,7 +2314,7 @@ mod tests { assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); assert_eq!( render_breaks(&text, &mut lines), @@ -2400,7 +2379,7 @@ mod tests { assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); } - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); assert_eq!( render_breaks(&text, &mut lines), @@ -2456,7 +2435,7 @@ mod tests { ["hello", "world toast and jam", "the end", "hi"] ); - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); { let layouts = lines.text_layouts.borrow(); @@ -2612,7 +2591,7 @@ mod tests { ["hello", "world toast and jam", "the end", "hi"] ); - lines.init_all(0, ConfigId::new(0, 0), &text_prov, true); + lines.init_all(ConfigId::new(0, 0), &text_prov, true); { let layouts = lines.text_layouts.borrow(); @@ -2971,7 +2950,7 @@ mod tests { .collect(); assert_eq!(r, vec!["bb ", "bb "]); - let v = lines.get_init_text_layout(0, ConfigId::new(0, 0), &text_prov, 2, true); + let v = lines.get_init_text_layout(ConfigId::new(0, 0), &text_prov, 2, true); let v = v.layout_cols(&text_prov, 2).collect::>(); assert_eq!(v, [(0, 3), (3, 8), (8, 13), (13, 15)]); let r: Vec<_> = lines @@ -3074,21 +3053,21 @@ mod tests { let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); let (text_prov, lines) = make_lines(&text, 2., false); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(0), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(0), true) .take(2) .map(|l| text.slice_to_cow(l.interval)) .collect(); assert_eq!(r, vec!["aaaa", "bb "]); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(1), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(1), true) .take(2) .map(|l| text.slice_to_cow(l.interval)) .collect(); assert_eq!(r, vec!["bb ", "bb "]); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(3), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(3), true) .take(3) .map(|l| text.slice_to_cow(l.interval)) .collect(); @@ -3098,17 +3077,17 @@ mod tests { let text: Rope = "".into(); let (text_prov, lines) = make_lines(&text, 2., false); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(0), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(0), true) .map(|l| text.slice_to_cow(l.interval)) .collect(); assert_eq!(r, vec![""]); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(1), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(1), true) .map(|l| text.slice_to_cow(l.interval)) .collect(); assert_eq!(r, Vec::<&str>::new()); let r: Vec<_> = lines - .iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(2), true) + .iter_vlines_init(&text_prov, ConfigId::new(0, 0), VLine(2), true) .map(|l| text.slice_to_cow(l.interval)) .collect(); assert_eq!(r, Vec::<&str>::new()); From 20db6bc32ce8dc5f00d5aeeec714a2215dcac14a Mon Sep 17 00:00:00 2001 From: MinusGix Date: Fri, 3 May 2024 12:10:11 -0500 Subject: [PATCH 20/21] Generalize to 'line render cache' This is useful for Lapce where we have styles/code-actions/etc associated with lines which can be partially invalidated. --- editor-core/src/buffer/mod.rs | 8 + src/views/editor/line_render_cache.rs | 290 +++++++++++++++++++++++++ src/views/editor/mod.rs | 1 + src/views/editor/visual_line.rs | 292 +------------------------- 4 files changed, 309 insertions(+), 282 deletions(-) create mode 100644 src/views/editor/line_render_cache.rs diff --git a/editor-core/src/buffer/mod.rs b/editor-core/src/buffer/mod.rs index eea2acb0..7b38ed45 100644 --- a/editor-core/src/buffer/mod.rs +++ b/editor-core/src/buffer/mod.rs @@ -85,6 +85,14 @@ impl InvalLines { new_count: 1, } } + + pub fn all(line_count: usize) -> Self { + Self { + start_line: 0, + inval_count: line_count, + new_count: line_count, + } + } } #[derive(Debug, Clone)] diff --git a/src/views/editor/line_render_cache.rs b/src/views/editor/line_render_cache.rs new file mode 100644 index 00000000..676534a6 --- /dev/null +++ b/src/views/editor/line_render_cache.rs @@ -0,0 +1,290 @@ +use floem_editor_core::buffer::InvalLines; + +/// Starts at a specific `base_line`, and then grows from there. +/// This is internally an array, so that newlines and moving the viewport up can be easily handled. +#[derive(Debug, Clone)] +pub struct LineRenderCache { + base_line: usize, + entries: Vec>, +} +impl LineRenderCache { + pub fn new() -> Self { + Self::default() + } + + fn idx(&self, line: usize) -> Option { + line.checked_sub(self.base_line) + } + + pub fn base_line(&self) -> usize { + self.base_line + } + + pub fn min_line(&self) -> usize { + self.base_line + } + + pub fn max_line(&self) -> Option { + if self.entries.is_empty() { + None + } else { + Some(self.min_line() + self.entries.len() - 1) + } + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn clear(&mut self) { + self.base_line = 0; + self.entries.clear(); + } + + pub fn get(&self, line: usize) -> Option<&T> { + let idx = self.idx(line)?; + self.entries.get(idx).map(|x| x.as_ref()).flatten() + } + + pub fn get_mut(&mut self, line: usize) -> Option<&mut T> { + let idx = self.idx(line)?; + self.entries.get_mut(idx).map(|x| x.as_mut()).flatten() + } + + pub fn insert(&mut self, line: usize, entry: T) { + if line < self.base_line { + let old_base = self.base_line; + self.base_line = line; + // Resize the entries at the start to fit the new count + let new_count = old_base - line; + self.entries + .splice(0..0, std::iter::repeat_with(|| None).take(new_count)); + } else if self.entries.is_empty() { + self.base_line = line; + self.entries.push(None); + } else if line >= self.base_line + self.entries.len() { + let new_len = line - self.base_line + 1; + self.entries.resize_with(new_len, || None); + } + let idx = self.idx(line).unwrap(); + let res = self.entries.get_mut(idx).unwrap(); + *res = Some(entry); + } + + /// Invalidates the entries at the given `start_line` for `inval_count` lines. + /// `new_count` is used to know whether to insert new line entries or to remove them, such as + /// for a newline. + pub fn invalidate( + &mut self, + InvalLines { + start_line, + inval_count, + new_count, + }: InvalLines, + ) { + let ib_start_line = start_line.max(self.base_line); + let start_idx = self.idx(ib_start_line).unwrap(); + + if start_idx >= self.entries.len() { + return; + } + + let end_idx = if start_line >= self.base_line { + start_idx + inval_count + } else { + // If start_line + inval_count isn't within the range of the entries then it'd just be 0 + let within_count = inval_count.saturating_sub(self.base_line - start_line); + start_idx + within_count + }; + let ib_end_idx = end_idx.min(self.entries.len()); + + for i in start_idx..ib_end_idx { + self.entries[i] = None; + } + + if new_count == inval_count { + return; + } + + if new_count > inval_count { + let extra = new_count - inval_count; + self.entries.splice( + ib_end_idx..ib_end_idx, + std::iter::repeat_with(|| None).take(extra), + ); + } else { + // How many (invalidated) line entries should be removed. + // (Since all of the lines in the inval lines area are `None` now, it doesn't matter if + // they were some other line number originally if we're draining them out) + let mut to_remove = inval_count; + let mut to_keep = new_count; + + let oob_start = ib_start_line - start_line; + + // Shift the base line backwards by the amount outside the start + // This allows us to not bother with removing entries from the array in some cases + { + let oob_start_remove = oob_start.min(to_remove); + + self.base_line -= oob_start_remove; + to_remove = to_remove.saturating_sub(oob_start_remove); + to_keep = to_keep.saturating_sub(oob_start_remove); + } + + if to_remove == 0 { + // There is nothing more to remove + return; + } + + let remove_start_idx = start_idx + to_keep; + let remove_end_idx = (start_idx + to_remove).min(self.entries.len()); + + self.entries.drain(remove_start_idx..remove_end_idx); + } + } + + pub fn iter(&self) -> impl Iterator> { + self.entries.iter().map(|x| x.as_ref()) + } + + pub fn iter_with_line(&self) -> impl Iterator)> { + let base_line = self.base_line(); + self.entries + .iter() + .enumerate() + .map(move |(i, x)| (i + base_line, x.as_ref())) + } +} + +impl Default for LineRenderCache { + fn default() -> Self { + LineRenderCache { + base_line: 0, + entries: Vec::new(), + } + } +} + +#[cfg(test)] +mod tests { + use floem_editor_core::buffer::InvalLines; + + use crate::views::editor::line_render_cache::LineRenderCache; + + #[test] + fn line_render_cache() { + let mut c = LineRenderCache::default(); + + assert_eq!(c.base_line, 0); + assert!(c.is_empty()); + + c.insert(0, 0); + assert_eq!(c.base_line, 0); + assert_eq!(c.entries.len(), 1); + + c.insert(1, 1); + assert_eq!(c.base_line, 0); + assert_eq!(c.entries.len(), 2); + + c.insert(10, 2); + assert_eq!(c.base_line, 0); + assert_eq!(c.entries.len(), 11); + + let mut c = LineRenderCache::default(); + c.insert(10, 10); + assert_eq!(c.base_line, 10); + assert_eq!(c.entries.len(), 1); + + c.insert(8, 8); + assert_eq!(c.base_line, 8); + assert_eq!(c.entries.len(), 3); + + c.insert(5, 5); + assert_eq!(c.base_line, 5); + assert_eq!(c.entries.len(), 6); + + assert!(c.get(0).is_none()); + assert!(c.get(5).is_some()); + assert!(c.get(8).is_some()); + assert!(c.get(10).is_some()); + assert!(c.get(11).is_none()); + + let mut c2 = c.clone(); + c2.invalidate(InvalLines::new(0, 1, 1)); + assert!(c2.get(0).is_none()); + assert!(c2.get(5).is_some()); + assert!(c2.get(8).is_some()); + assert!(c2.get(10).is_some()); + assert!(c2.get(11).is_none()); + + let mut c2 = c.clone(); + c2.invalidate(InvalLines::new(5, 1, 1)); + assert!(c2.get(0).is_none()); + assert!(c2.get(5).is_none()); + assert!(c2.get(8).is_some()); + assert!(c2.get(10).is_some()); + assert!(c2.get(11).is_none()); + + c.invalidate(InvalLines::new(0, 6, 6)); + assert!(c.get(5).is_none()); + assert!(c.get(8).is_some()); + assert!(c.get(10).is_some()); + assert!(c.get(11).is_none()); + + let mut c = LineRenderCache::default(); + for i in 0..10 { + c.insert(i, i); + } + + assert_eq!(c.base_line, 0); + assert_eq!(c.entries.len(), 10); + + c.invalidate(InvalLines::new(0, 10, 1)); + assert!(c.get(0).is_none()); + assert_eq!(c.len(), 1); + + let mut c = LineRenderCache::default(); + for i in 0..10 { + c.insert(i, i); + } + + c.invalidate(InvalLines::new(5, 800, 1)); + assert!(c.get(0).is_some()); + assert!(c.get(1).is_some()); + assert!(c.get(2).is_some()); + assert!(c.get(3).is_some()); + assert!(c.get(4).is_some()); + assert_eq!(c.len(), 6); + + let mut c = LineRenderCache::default(); + for i in 5..10 { + c.insert(i, i); + } + + assert_eq!(c.base_line, 5); + + c.invalidate(InvalLines::new(0, 7, 1)); + assert_eq!(c.base_line, 0); + assert!(c.get(0).is_some()); // was line 7 + assert!(c.get(1).is_some()); // was line 8 + assert!(c.get(2).is_some()); // was line 9 + assert!(c.get(3).is_none()); + assert!(c.get(4).is_none()); + assert_eq!(c.len(), 3); + + let mut c = LineRenderCache::default(); + for i in 0..10 { + c.insert(i, i); + } + + c.invalidate(InvalLines::new(0, 800, 1)); + assert!(c.get(0).is_none()); + assert_eq!(c.len(), 1); + + // TODO: test the contents + } +} diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs index ea815b5f..17efb7df 100644 --- a/src/views/editor/mod.rs +++ b/src/views/editor/mod.rs @@ -34,6 +34,7 @@ pub mod gutter; pub mod id; pub mod keypress; pub mod layout; +pub mod line_render_cache; pub mod listener; pub mod movement; pub mod phantom_text; diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs index 77f2b8c6..f379cba8 100644 --- a/src/views/editor/visual_line.rs +++ b/src/views/editor/visual_line.rs @@ -75,7 +75,7 @@ use floem_renderer::cosmic_text::{HitPosition, LayoutGlyph, TextLayout}; use lapce_xi_rope::{Interval, Rope}; use peniko::kurbo::Point; -use super::{layout::TextLayoutLine, listener::Listener}; +use super::{layout::TextLayoutLine, line_render_cache::LineRenderCache, listener::Listener}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum ResolvedWrap { @@ -123,145 +123,6 @@ impl RVLine { } } -/// The cached text layouts. -/// Starts at a specific `base_line`, and then grows from there. -/// This is internally an array, so that newlines and moving the viewport up can be easily handled. -#[derive(Default, Clone)] -pub struct Layouts { - base_line: usize, - layouts: Vec>>, -} -impl Layouts { - fn idx(&self, line: usize) -> Option { - line.checked_sub(self.base_line) - } - - pub fn min_line(&self) -> usize { - self.base_line - } - - pub fn max_line(&self) -> Option { - if self.layouts.is_empty() { - None - } else { - Some(self.min_line() + self.layouts.len() - 1) - } - } - - pub fn len(&self) -> usize { - self.layouts.len() - } - - pub fn is_empty(&self) -> bool { - self.layouts.is_empty() - } - - pub fn clear(&mut self) { - self.base_line = 0; - self.layouts.clear(); - } - - pub fn get(&self, line: usize) -> Option<&Arc> { - let idx = self.idx(line)?; - self.layouts.get(idx).map(|x| x.as_ref()).flatten() - } - - pub fn get_mut(&mut self, line: usize) -> Option<&mut Arc> { - let idx = self.idx(line)?; - self.layouts.get_mut(idx).map(|x| x.as_mut()).flatten() - } - - pub fn insert(&mut self, line: usize, layout: Arc) { - if line < self.base_line { - let old_base = self.base_line; - self.base_line = line; - // Resize the layouts at the start to fit the new count - let new_count = old_base - line; - self.layouts - .splice(0..0, std::iter::repeat(None).take(new_count)); - } else if self.layouts.is_empty() { - self.base_line = line; - self.layouts.push(None); - } else if line >= self.base_line + self.layouts.len() { - let new_len = line - self.base_line + 1; - self.layouts.resize(new_len, None); - } - let idx = self.idx(line).unwrap(); - let res = self.layouts.get_mut(idx).unwrap(); - *res = Some(layout); - } - - /// Invalidates the layouts at the given `start_line` for `inval_count` lines. - /// `new_count` is used to know whether to insert new line entries or to remove them, such as - /// for a newline. - pub fn invalidate( - &mut self, - InvalLines { - start_line, - inval_count, - new_count, - }: InvalLines, - ) { - let ib_start_line = start_line.max(self.base_line); - let start_idx = self.idx(ib_start_line).unwrap(); - - if start_idx >= self.layouts.len() { - return; - } - - let end_idx = if start_line >= self.base_line { - start_idx + inval_count - } else { - // If start_line + inval_count isn't within the range of the layouts then it'd just be 0 - let within_count = inval_count.saturating_sub(self.base_line - start_line); - start_idx + within_count - }; - let ib_end_idx = end_idx.min(self.layouts.len()); - - for i in start_idx..ib_end_idx { - self.layouts[i] = None; - } - - if new_count == inval_count { - return; - } - - if new_count > inval_count { - let extra = new_count - inval_count; - self.layouts - .splice(ib_end_idx..ib_end_idx, std::iter::repeat(None).take(extra)); - } else { - // How many (invalidated) line entries should be removed. - // (Since all of the lines in the inval lines area are `None` now, it doesn't matter if - // they were some other line number originally if we're draining them out) - let mut to_remove = inval_count; - let mut to_keep = new_count; - - let oob_start = ib_start_line - start_line; - - // Shift the base line backwards by the amount outside the start - // This allows us to not bother with removing entries from the array in some cases - { - let oob_start_remove = oob_start.min(to_remove); - - self.base_line -= oob_start_remove; - to_remove = to_remove.saturating_sub(oob_start_remove); - to_keep = to_keep.saturating_sub(oob_start_remove); - } - - if to_remove == 0 { - // There is nothing more to remove - return; - } - - let remove_start_idx = start_idx + to_keep; - let remove_end_idx = (start_idx + to_remove).min(self.layouts.len()); - - drop(self.layouts.drain(remove_start_idx..remove_end_idx)); - } - } -} - #[derive(Debug, Default, PartialEq, Clone, Copy)] pub struct ConfigId { editor_style_id: u64, @@ -281,7 +142,7 @@ pub struct TextLayoutCache { /// The id of the last config so that we can clear when the config changes /// the first is the styling id and the second is an id for changes from Floem style config_id: ConfigId, - pub layouts: Layouts, + pub layouts: LineRenderCache>, /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar pub max_width: f64, } @@ -491,20 +352,20 @@ impl Lines { let layouts = self.text_layouts.borrow(); let layouts = &layouts.layouts; - let base_line = layouts.base_line; + let base_line = layouts.base_line(); // Before the layouts baseline, there is #base_line non-wrapped lines soft_line_count += base_line; // Add all the potentially wrapped line counts - for entry in layouts.layouts.iter() { + for entry in layouts.iter() { let line_count = entry.as_ref().map(|l| l.line_count()).unwrap_or(1); soft_line_count += line_count; } // Add all the lines after the layouts - let after = base_line + layouts.layouts.len(); + let after = base_line + layouts.len(); let diff = hard_line_count - after; soft_line_count += diff; @@ -1299,7 +1160,7 @@ pub fn find_vline_init_info( let layouts = lines.text_layouts.borrow(); let layouts = &layouts.layouts; - let base_line = layouts.base_line; + let base_line = layouts.base_line(); if base_line > vline.get() { // The vline is not within the rendered, thus it is linear @@ -1308,9 +1169,7 @@ pub fn find_vline_init_info( } let mut cur_vline = base_line; - for (i, entry) in layouts.layouts.iter().enumerate() { - let cur_line = base_line + i; - + for (cur_line, entry) in layouts.iter_with_line() { let line_count = entry.as_ref().map(|l| l.line_count()).unwrap_or(1); if cur_vline + line_count > vline.get() { @@ -1332,7 +1191,7 @@ pub fn find_vline_init_info( cur_vline += line_count; } - let cur_line = base_line + layouts.layouts.len(); + let cur_line = base_line + layouts.len(); let linear_diff = vline.get() - cur_vline; @@ -1919,10 +1778,7 @@ mod tests { use std::{borrow::Cow, collections::HashMap, sync::Arc}; use floem_editor_core::{ - buffer::{ - rope_text::{RopeText, RopeTextRef, RopeTextVal}, - InvalLines, - }, + buffer::rope_text::{RopeText, RopeTextRef, RopeTextVal}, cursor::CursorAffinity, }; use floem_reactive::Scope; @@ -1937,8 +1793,7 @@ mod tests { }; use super::{ - find_vline_init_info, ConfigId, Layouts, Lines, RVLine, ResolvedWrap, TextLayoutProvider, - VLine, + find_vline_init_info, ConfigId, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine, }; /// For most of the logic we standardize on a specific font size. @@ -3346,131 +3201,4 @@ mod tests { "simple multiline (CRLF)", ); } - - #[test] - fn layout_cache() { - let random_layout = |text: &str| { - let mut text_layout = TextLayout::new(); - text_layout.set_text(text, AttrsList::new(Attrs::new())); - - Arc::new(TextLayoutLine { - extra_style: Vec::new(), - text: text_layout, - whitespaces: None, - indent: 0., - phantom_text: PhantomTextLine::default(), - }) - }; - let mut layouts = Layouts::default(); - - assert_eq!(layouts.base_line, 0); - assert!(layouts.layouts.is_empty()); - - layouts.insert(0, random_layout("abc")); - assert_eq!(layouts.base_line, 0); - assert_eq!(layouts.layouts.len(), 1); - - layouts.insert(1, random_layout("def")); - assert_eq!(layouts.base_line, 0); - assert_eq!(layouts.layouts.len(), 2); - - layouts.insert(10, random_layout("ghi")); - assert_eq!(layouts.base_line, 0); - assert_eq!(layouts.layouts.len(), 11); - - let mut layouts = Layouts::default(); - layouts.insert(10, random_layout("ghi")); - assert_eq!(layouts.base_line, 10); - assert_eq!(layouts.layouts.len(), 1); - - layouts.insert(8, random_layout("jkl")); - assert_eq!(layouts.base_line, 8); - assert_eq!(layouts.layouts.len(), 3); - - layouts.insert(5, random_layout("mno")); - assert_eq!(layouts.base_line, 5); - assert_eq!(layouts.layouts.len(), 6); - - assert!(layouts.get(0).is_none()); - assert!(layouts.get(5).is_some()); - assert!(layouts.get(8).is_some()); - assert!(layouts.get(10).is_some()); - assert!(layouts.get(11).is_none()); - - let mut layouts2 = layouts.clone(); - layouts2.invalidate(InvalLines::new(0, 1, 1)); - assert!(layouts2.get(0).is_none()); - assert!(layouts2.get(5).is_some()); - assert!(layouts2.get(8).is_some()); - assert!(layouts2.get(10).is_some()); - assert!(layouts2.get(11).is_none()); - - let mut layouts2 = layouts.clone(); - layouts2.invalidate(InvalLines::new(5, 1, 1)); - assert!(layouts2.get(0).is_none()); - assert!(layouts2.get(5).is_none()); - assert!(layouts2.get(8).is_some()); - assert!(layouts2.get(10).is_some()); - assert!(layouts2.get(11).is_none()); - - layouts.invalidate(InvalLines::new(0, 6, 6)); - assert!(layouts.get(5).is_none()); - assert!(layouts.get(8).is_some()); - assert!(layouts.get(10).is_some()); - assert!(layouts.get(11).is_none()); - - let mut layouts = Layouts::default(); - for i in 0..10 { - let text = format!("{}", i); - layouts.insert(i, random_layout(&text)); - } - - assert_eq!(layouts.base_line, 0); - assert_eq!(layouts.layouts.len(), 10); - - layouts.invalidate(InvalLines::new(0, 10, 1)); - assert!(layouts.get(0).is_none()); - assert_eq!(layouts.len(), 1); - - let mut layouts = Layouts::default(); - for i in 0..10 { - let text = format!("{}", i); - layouts.insert(i, random_layout(&text)); - } - - layouts.invalidate(InvalLines::new(5, 800, 1)); - assert!(layouts.get(0).is_some()); - assert!(layouts.get(1).is_some()); - assert!(layouts.get(2).is_some()); - assert!(layouts.get(3).is_some()); - assert!(layouts.get(4).is_some()); - assert_eq!(layouts.len(), 6); - - let mut layouts = Layouts::default(); - for i in 5..10 { - let text = format!("{}", i); - layouts.insert(i, random_layout(&text)); - } - - assert_eq!(layouts.base_line, 5); - - layouts.invalidate(InvalLines::new(0, 7, 1)); - assert_eq!(layouts.base_line, 0); - assert!(layouts.get(0).is_some()); // was line 7 - assert!(layouts.get(1).is_some()); // was line 8 - assert!(layouts.get(2).is_some()); // was line 9 - assert!(layouts.get(3).is_none()); - assert!(layouts.get(4).is_none()); - assert_eq!(layouts.len(), 3); - - let mut layouts = Layouts::default(); - for i in 0..10 { - let text = format!("{}", i); - layouts.insert(i, random_layout(&text)); - } - - layouts.invalidate(InvalLines::new(0, 800, 1)); - assert!(layouts.get(0).is_none()); - assert_eq!(layouts.len(), 1); - } } From 4e41bcaec3a7ad191b1e2136adb01964ecb8dccd Mon Sep 17 00:00:00 2001 From: MinusGix Date: Thu, 20 Jun 2024 22:49:38 -0500 Subject: [PATCH 21/21] Temp criterion commit --- Cargo.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d4022d9e..dd844811 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,9 @@ image = { workspace = true } im = { workspace = true } wgpu = { workspace = true } +[dev-dependencies] +criterion = "0.5" + [features] default = ["editor", "default-image-formats"] # TODO: this is only winit and the editor serde, there are other dependencies that still depend on @@ -99,3 +102,10 @@ tokio = ["dep:tokio"] # rfd (file dialog) async runtime rfd-async-std = ["dep:rfd", "rfd/async-std"] rfd-tokio = ["dep:rfd", "rfd/tokio"] + +[profile.bench] +debug = true + +[[bench]] +name = "basic_editing" +harness = false