1use std::collections::HashMap;
15use std::ops::Range;
16
17use twrite_core::{ConcealedLine, EditorBuffer, StyleSpan, SyntaxHighlighter};
18
19const MAX_CACHED_ROWS: usize = 2048;
22
23#[derive(Debug, Clone)]
25pub struct CachedInput {
26 pub spans: Vec<StyleSpan>,
28 pub concealed: ConcealedLine,
33 pub link_src: Vec<(Range<usize>, String)>,
35 pub allow_wrap: bool,
37}
38
39#[derive(Debug, Clone)]
40struct CachedRow {
41 active: bool,
44 input: CachedInput,
45}
46
47#[derive(Debug, Default)]
49pub struct LayoutCache {
50 version: Option<usize>,
51 highlighter_rev: Option<u64>,
52 rows: HashMap<usize, CachedRow>,
53 hits: u64,
54 misses: u64,
55}
56
57impl LayoutCache {
58 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn clear(&mut self) {
65 self.rows.clear();
66 self.version = None;
67 self.highlighter_rev = None;
68 self.hits = 0;
69 self.misses = 0;
70 }
71
72 pub fn stats(&self) -> (u64, u64) {
74 (self.hits, self.misses)
75 }
76
77 pub fn len(&self) -> usize {
79 self.rows.len()
80 }
81
82 pub fn is_empty(&self) -> bool {
84 self.rows.is_empty()
85 }
86
87 pub fn cached_input(
93 &mut self,
94 buffer: &EditorBuffer,
95 highlighter: Option<&dyn SyntaxHighlighter>,
96 highlighter_rev: u64,
97 cursor_row: usize,
98 row: usize,
99 line_text: &str,
100 ) -> &CachedInput {
101 let version = buffer.version();
102 if self.version != Some(version) || self.highlighter_rev != Some(highlighter_rev) {
103 self.rows.clear();
104 self.version = Some(version);
105 self.highlighter_rev = Some(highlighter_rev);
106 }
107 let active = row == cursor_row;
108 if let Some(cached) = self.rows.get(&row)
109 && cached.active == active
110 {
111 self.hits += 1;
112 return &self.rows.get(&row).expect("row present").input;
114 }
115 self.misses += 1;
116 if self.rows.len() >= MAX_CACHED_ROWS {
117 self.rows.clear();
118 }
119 let spans = highlighter
120 .map(|h| h.highlight_line(buffer, row, line_text))
121 .unwrap_or_default();
122 let allow_wrap = highlighter
123 .map(|h| h.should_wrap_line(buffer, row))
124 .unwrap_or(true);
125 let mut concealed = ConcealedLine::build(line_text, &spans);
126 let pads = highlighter
127 .map(|h| h.expand_line(buffer, row, &concealed))
128 .unwrap_or_default();
129 if !pads.is_empty() {
130 concealed = concealed.expanded(&pads);
131 }
132 let link_src = highlighter
133 .map(|h| h.extract_links(buffer, row, line_text))
134 .unwrap_or_default();
135 self.rows.insert(
136 row,
137 CachedRow {
138 active,
139 input: CachedInput {
140 spans,
141 concealed,
142 link_src,
143 allow_wrap,
144 },
145 },
146 );
147 &self.rows.get(&row).expect("row just inserted").input
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn empty_buffer(lines: usize) -> EditorBuffer {
156 let text = (0..lines)
157 .map(|i| format!("line {i}"))
158 .collect::<Vec<_>>()
159 .join("\n");
160 EditorBuffer::new(&text)
161 }
162
163 #[test]
164 fn second_pass_is_all_hits() {
165 let buf = empty_buffer(50);
166 let mut cache = LayoutCache::new();
167 for row in 0..buf.len_lines() {
168 let line = buf.line_to_string(row);
169 let text = line.trim_end_matches(['\r', '\n']);
170 cache.cached_input(&buf, None, 0, usize::MAX, row, text);
171 }
172 assert_eq!(cache.stats(), (0, 50));
173 for row in 0..buf.len_lines() {
174 let line = buf.line_to_string(row);
175 let text = line.trim_end_matches(['\r', '\n']);
176 cache.cached_input(&buf, None, 0, usize::MAX, row, text);
177 }
178 assert_eq!(cache.stats(), (50, 50));
179 assert_eq!(cache.len(), 50);
180 }
181
182 #[test]
183 fn version_bump_invalidates() {
184 let mut buf = empty_buffer(10);
185 let mut cache = LayoutCache::new();
186 let line = buf.line_to_string(0);
187 let text = line.trim_end_matches(['\r', '\n']).to_string();
188 cache.cached_input(&buf, None, 0, usize::MAX, 0, &text);
189 assert_eq!(cache.stats(), (0, 1));
190 buf.insert("x");
191 let line = buf.line_to_string(0);
192 let text = line.trim_end_matches(['\r', '\n']).to_string();
193 cache.cached_input(&buf, None, 0, usize::MAX, 0, &text);
194 assert_eq!(cache.stats(), (0, 2));
196 assert_eq!(cache.len(), 1);
197 }
198
199 #[test]
200 fn cursor_row_flip_recomputes_only_flipped_rows() {
201 let buf = empty_buffer(4);
202 let mut cache = LayoutCache::new();
203 for row in 0..4 {
204 let line = buf.line_to_string(row);
205 let text = line.trim_end_matches(['\r', '\n']).to_string();
206 cache.cached_input(&buf, None, 0, 0, row, &text);
207 }
208 assert_eq!(cache.stats(), (0, 4));
209 for row in 0..4 {
211 let line = buf.line_to_string(row);
212 let text = line.trim_end_matches(['\r', '\n']).to_string();
213 cache.cached_input(&buf, None, 0, 0, row, &text);
214 }
215 assert_eq!(cache.stats(), (4, 4));
216 for row in 0..4 {
218 let line = buf.line_to_string(row);
219 let text = line.trim_end_matches(['\r', '\n']).to_string();
220 cache.cached_input(&buf, None, 0, 1, row, &text);
221 }
222 assert_eq!(cache.stats(), (6, 6));
223 }
224
225 #[test]
226 fn highlighter_rev_bump_invalidates() {
227 let buf = empty_buffer(5);
228 let mut cache = LayoutCache::new();
229 for row in 0..5 {
230 let line = buf.line_to_string(row);
231 let text = line.trim_end_matches(['\r', '\n']).to_string();
232 cache.cached_input(&buf, None, 0, usize::MAX, row, &text);
233 }
234 assert_eq!(cache.len(), 5);
235 let line = buf.line_to_string(0);
236 let text = line.trim_end_matches(['\r', '\n']).to_string();
237 cache.cached_input(&buf, None, 1, usize::MAX, 0, &text);
238 assert_eq!(cache.len(), 1);
239 }
240
241 #[test]
242 fn clear_resets_stats() {
243 let buf = empty_buffer(3);
244 let mut cache = LayoutCache::new();
245 let line = buf.line_to_string(0);
246 let text = line.trim_end_matches(['\r', '\n']).to_string();
247 cache.cached_input(&buf, None, 0, usize::MAX, 0, &text);
248 cache.clear();
249 assert_eq!(cache.stats(), (0, 0));
250 assert!(cache.is_empty());
251 }
252}