Skip to content

Commit 6d24a00

Browse files
committed
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.
1 parent c0402a6 commit 6d24a00

File tree

4 files changed

+309
-282
lines changed

4 files changed

+309
-282
lines changed

editor-core/src/buffer/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ impl InvalLines {
8484
new_count: 1,
8585
}
8686
}
87+
88+
pub fn all(line_count: usize) -> Self {
89+
Self {
90+
start_line: 0,
91+
inval_count: line_count,
92+
new_count: line_count,
93+
}
94+
}
8795
}
8896

8997
#[derive(Debug, Clone)]

src/views/editor/line_render_cache.rs

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
use floem_editor_core::buffer::InvalLines;
2+
3+
/// Starts at a specific `base_line`, and then grows from there.
4+
/// This is internally an array, so that newlines and moving the viewport up can be easily handled.
5+
#[derive(Debug, Clone)]
6+
pub struct LineRenderCache<T> {
7+
base_line: usize,
8+
entries: Vec<Option<T>>,
9+
}
10+
impl<T> LineRenderCache<T> {
11+
pub fn new() -> Self {
12+
Self::default()
13+
}
14+
15+
fn idx(&self, line: usize) -> Option<usize> {
16+
line.checked_sub(self.base_line)
17+
}
18+
19+
pub fn base_line(&self) -> usize {
20+
self.base_line
21+
}
22+
23+
pub fn min_line(&self) -> usize {
24+
self.base_line
25+
}
26+
27+
pub fn max_line(&self) -> Option<usize> {
28+
if self.entries.is_empty() {
29+
None
30+
} else {
31+
Some(self.min_line() + self.entries.len() - 1)
32+
}
33+
}
34+
35+
pub fn len(&self) -> usize {
36+
self.entries.len()
37+
}
38+
39+
pub fn is_empty(&self) -> bool {
40+
self.entries.is_empty()
41+
}
42+
43+
pub fn clear(&mut self) {
44+
self.base_line = 0;
45+
self.entries.clear();
46+
}
47+
48+
pub fn get(&self, line: usize) -> Option<&T> {
49+
let idx = self.idx(line)?;
50+
self.entries.get(idx).map(|x| x.as_ref()).flatten()
51+
}
52+
53+
pub fn get_mut(&mut self, line: usize) -> Option<&mut T> {
54+
let idx = self.idx(line)?;
55+
self.entries.get_mut(idx).map(|x| x.as_mut()).flatten()
56+
}
57+
58+
pub fn insert(&mut self, line: usize, entry: T) {
59+
if line < self.base_line {
60+
let old_base = self.base_line;
61+
self.base_line = line;
62+
// Resize the entries at the start to fit the new count
63+
let new_count = old_base - line;
64+
self.entries
65+
.splice(0..0, std::iter::repeat_with(|| None).take(new_count));
66+
} else if self.entries.is_empty() {
67+
self.base_line = line;
68+
self.entries.push(None);
69+
} else if line >= self.base_line + self.entries.len() {
70+
let new_len = line - self.base_line + 1;
71+
self.entries.resize_with(new_len, || None);
72+
}
73+
let idx = self.idx(line).unwrap();
74+
let res = self.entries.get_mut(idx).unwrap();
75+
*res = Some(entry);
76+
}
77+
78+
/// Invalidates the entries at the given `start_line` for `inval_count` lines.
79+
/// `new_count` is used to know whether to insert new line entries or to remove them, such as
80+
/// for a newline.
81+
pub fn invalidate(
82+
&mut self,
83+
InvalLines {
84+
start_line,
85+
inval_count,
86+
new_count,
87+
}: InvalLines,
88+
) {
89+
let ib_start_line = start_line.max(self.base_line);
90+
let start_idx = self.idx(ib_start_line).unwrap();
91+
92+
if start_idx >= self.entries.len() {
93+
return;
94+
}
95+
96+
let end_idx = if start_line >= self.base_line {
97+
start_idx + inval_count
98+
} else {
99+
// If start_line + inval_count isn't within the range of the entries then it'd just be 0
100+
let within_count = inval_count.saturating_sub(self.base_line - start_line);
101+
start_idx + within_count
102+
};
103+
let ib_end_idx = end_idx.min(self.entries.len());
104+
105+
for i in start_idx..ib_end_idx {
106+
self.entries[i] = None;
107+
}
108+
109+
if new_count == inval_count {
110+
return;
111+
}
112+
113+
if new_count > inval_count {
114+
let extra = new_count - inval_count;
115+
self.entries.splice(
116+
ib_end_idx..ib_end_idx,
117+
std::iter::repeat_with(|| None).take(extra),
118+
);
119+
} else {
120+
// How many (invalidated) line entries should be removed.
121+
// (Since all of the lines in the inval lines area are `None` now, it doesn't matter if
122+
// they were some other line number originally if we're draining them out)
123+
let mut to_remove = inval_count;
124+
let mut to_keep = new_count;
125+
126+
let oob_start = ib_start_line - start_line;
127+
128+
// Shift the base line backwards by the amount outside the start
129+
// This allows us to not bother with removing entries from the array in some cases
130+
{
131+
let oob_start_remove = oob_start.min(to_remove);
132+
133+
self.base_line -= oob_start_remove;
134+
to_remove = to_remove.saturating_sub(oob_start_remove);
135+
to_keep = to_keep.saturating_sub(oob_start_remove);
136+
}
137+
138+
if to_remove == 0 {
139+
// There is nothing more to remove
140+
return;
141+
}
142+
143+
let remove_start_idx = start_idx + to_keep;
144+
let remove_end_idx = (start_idx + to_remove).min(self.entries.len());
145+
146+
self.entries.drain(remove_start_idx..remove_end_idx);
147+
}
148+
}
149+
150+
pub fn iter(&self) -> impl Iterator<Item = Option<&T>> {
151+
self.entries.iter().map(|x| x.as_ref())
152+
}
153+
154+
pub fn iter_with_line(&self) -> impl Iterator<Item = (usize, Option<&T>)> {
155+
let base_line = self.base_line();
156+
self.entries
157+
.iter()
158+
.enumerate()
159+
.map(move |(i, x)| (i + base_line, x.as_ref()))
160+
}
161+
}
162+
163+
impl<T> Default for LineRenderCache<T> {
164+
fn default() -> Self {
165+
LineRenderCache {
166+
base_line: 0,
167+
entries: Vec::new(),
168+
}
169+
}
170+
}
171+
172+
#[cfg(test)]
173+
mod tests {
174+
use floem_editor_core::buffer::InvalLines;
175+
176+
use crate::views::editor::line_render_cache::LineRenderCache;
177+
178+
#[test]
179+
fn line_render_cache() {
180+
let mut c = LineRenderCache::default();
181+
182+
assert_eq!(c.base_line, 0);
183+
assert!(c.is_empty());
184+
185+
c.insert(0, 0);
186+
assert_eq!(c.base_line, 0);
187+
assert_eq!(c.entries.len(), 1);
188+
189+
c.insert(1, 1);
190+
assert_eq!(c.base_line, 0);
191+
assert_eq!(c.entries.len(), 2);
192+
193+
c.insert(10, 2);
194+
assert_eq!(c.base_line, 0);
195+
assert_eq!(c.entries.len(), 11);
196+
197+
let mut c = LineRenderCache::default();
198+
c.insert(10, 10);
199+
assert_eq!(c.base_line, 10);
200+
assert_eq!(c.entries.len(), 1);
201+
202+
c.insert(8, 8);
203+
assert_eq!(c.base_line, 8);
204+
assert_eq!(c.entries.len(), 3);
205+
206+
c.insert(5, 5);
207+
assert_eq!(c.base_line, 5);
208+
assert_eq!(c.entries.len(), 6);
209+
210+
assert!(c.get(0).is_none());
211+
assert!(c.get(5).is_some());
212+
assert!(c.get(8).is_some());
213+
assert!(c.get(10).is_some());
214+
assert!(c.get(11).is_none());
215+
216+
let mut c2 = c.clone();
217+
c2.invalidate(InvalLines::new(0, 1, 1));
218+
assert!(c2.get(0).is_none());
219+
assert!(c2.get(5).is_some());
220+
assert!(c2.get(8).is_some());
221+
assert!(c2.get(10).is_some());
222+
assert!(c2.get(11).is_none());
223+
224+
let mut c2 = c.clone();
225+
c2.invalidate(InvalLines::new(5, 1, 1));
226+
assert!(c2.get(0).is_none());
227+
assert!(c2.get(5).is_none());
228+
assert!(c2.get(8).is_some());
229+
assert!(c2.get(10).is_some());
230+
assert!(c2.get(11).is_none());
231+
232+
c.invalidate(InvalLines::new(0, 6, 6));
233+
assert!(c.get(5).is_none());
234+
assert!(c.get(8).is_some());
235+
assert!(c.get(10).is_some());
236+
assert!(c.get(11).is_none());
237+
238+
let mut c = LineRenderCache::default();
239+
for i in 0..10 {
240+
c.insert(i, i);
241+
}
242+
243+
assert_eq!(c.base_line, 0);
244+
assert_eq!(c.entries.len(), 10);
245+
246+
c.invalidate(InvalLines::new(0, 10, 1));
247+
assert!(c.get(0).is_none());
248+
assert_eq!(c.len(), 1);
249+
250+
let mut c = LineRenderCache::default();
251+
for i in 0..10 {
252+
c.insert(i, i);
253+
}
254+
255+
c.invalidate(InvalLines::new(5, 800, 1));
256+
assert!(c.get(0).is_some());
257+
assert!(c.get(1).is_some());
258+
assert!(c.get(2).is_some());
259+
assert!(c.get(3).is_some());
260+
assert!(c.get(4).is_some());
261+
assert_eq!(c.len(), 6);
262+
263+
let mut c = LineRenderCache::default();
264+
for i in 5..10 {
265+
c.insert(i, i);
266+
}
267+
268+
assert_eq!(c.base_line, 5);
269+
270+
c.invalidate(InvalLines::new(0, 7, 1));
271+
assert_eq!(c.base_line, 0);
272+
assert!(c.get(0).is_some()); // was line 7
273+
assert!(c.get(1).is_some()); // was line 8
274+
assert!(c.get(2).is_some()); // was line 9
275+
assert!(c.get(3).is_none());
276+
assert!(c.get(4).is_none());
277+
assert_eq!(c.len(), 3);
278+
279+
let mut c = LineRenderCache::default();
280+
for i in 0..10 {
281+
c.insert(i, i);
282+
}
283+
284+
c.invalidate(InvalLines::new(0, 800, 1));
285+
assert!(c.get(0).is_none());
286+
assert_eq!(c.len(), 1);
287+
288+
// TODO: test the contents
289+
}
290+
}

src/views/editor/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub mod gutter;
3434
pub mod id;
3535
pub mod keypress;
3636
pub mod layout;
37+
pub mod line_render_cache;
3738
pub mod listener;
3839
pub mod movement;
3940
pub mod phantom_text;

0 commit comments

Comments
 (0)