1use crate::model::marker::{MarkerId, MarkerList};
22use fresh_core::overlay::OverlayNamespace;
23use std::collections::HashMap;
24
25#[derive(Debug, Clone)]
27pub struct SoftBreakPoint {
28 pub namespace: OverlayNamespace,
30
31 pub marker_id: MarkerId,
33
34 pub indent: u16,
36}
37
38impl SoftBreakPoint {
39 pub fn position(&self, marker_list: &MarkerList) -> usize {
41 marker_list.get_position(self.marker_id).unwrap_or(0)
42 }
43
44 pub fn in_range(&self, start: usize, end: usize, marker_list: &MarkerList) -> bool {
46 let pos = self.position(marker_list);
47 pos >= start && pos < end
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct SoftBreakManager {
54 breaks: Vec<SoftBreakPoint>,
55 marker_to_idx: HashMap<MarkerId, usize>,
59 version: u32,
63}
64
65impl SoftBreakManager {
66 pub fn new() -> Self {
68 Self {
69 breaks: Vec::new(),
70 marker_to_idx: HashMap::new(),
71 version: 0,
72 }
73 }
74
75 pub fn version(&self) -> u32 {
77 self.version
78 }
79
80 pub fn add(
82 &mut self,
83 marker_list: &mut MarkerList,
84 namespace: OverlayNamespace,
85 position: usize,
86 indent: u16,
87 ) {
88 let marker_id = marker_list.create(position, false); let idx = self.breaks.len();
91 self.marker_to_idx.insert(marker_id, idx);
92 self.breaks.push(SoftBreakPoint {
93 namespace,
94 marker_id,
95 indent,
96 });
97 self.version = self.version.wrapping_add(1);
98 }
99
100 pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
102 let mut indices: Vec<usize> = self
103 .breaks
104 .iter()
105 .enumerate()
106 .filter_map(|(i, b)| (&b.namespace == namespace).then_some(i))
107 .collect();
108 if indices.is_empty() {
109 return;
110 }
111 indices.sort_unstable_by(|a, b| b.cmp(a));
112 for idx in indices {
113 self.swap_remove_at(idx, marker_list);
114 }
115 self.version = self.version.wrapping_add(1);
116 }
117
118 pub fn remove_in_range(&mut self, start: usize, end: usize, marker_list: &mut MarkerList) {
120 if start >= end {
124 return;
125 }
126 let hits = marker_list.query_range(start, end);
127 if hits.is_empty() {
128 return;
129 }
130 let mut to_remove: Vec<usize> = hits
131 .iter()
132 .filter_map(|(mid, pos, _)| {
133 if *pos < end {
134 self.marker_to_idx.get(mid).copied()
135 } else {
136 None
137 }
138 })
139 .collect();
140 to_remove.sort_unstable();
141 to_remove.dedup();
142 if to_remove.is_empty() {
143 return;
144 }
145 to_remove.sort_unstable_by(|a, b| b.cmp(a));
147 for idx in to_remove {
148 self.swap_remove_at(idx, marker_list);
149 }
150 self.version = self.version.wrapping_add(1);
151 }
152
153 pub fn clear(&mut self, marker_list: &mut MarkerList) {
155 let had_any = !self.breaks.is_empty();
156 for bp in &self.breaks {
157 marker_list.delete(bp.marker_id);
158 }
159 self.breaks.clear();
160 self.marker_to_idx.clear();
161 if had_any {
162 self.version = self.version.wrapping_add(1);
163 }
164 }
165
166 fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
169 let removed = self.breaks.swap_remove(idx);
170 self.marker_to_idx.remove(&removed.marker_id);
171 marker_list.delete(removed.marker_id);
172 if let Some(moved) = self.breaks.get(idx) {
173 self.marker_to_idx.insert(moved.marker_id, idx);
174 }
175 }
176
177 pub fn query_viewport(
180 &self,
181 start: usize,
182 end: usize,
183 marker_list: &MarkerList,
184 ) -> Vec<(usize, u16)> {
185 let mut results: Vec<(usize, u16)> = self
186 .breaks
187 .iter()
188 .filter_map(|b| {
189 let pos = b.position(marker_list);
190 if pos >= start && pos < end {
191 Some((pos, b.indent))
192 } else {
193 None
194 }
195 })
196 .collect();
197
198 results.sort_by_key(|(pos, _)| *pos);
200
201 results
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.breaks.is_empty()
207 }
208
209 #[cfg(test)]
212 fn check_invariants(&self) {
213 assert_eq!(
214 self.marker_to_idx.len(),
215 self.breaks.len(),
216 "marker_to_idx size != breaks size"
217 );
218 for (i, b) in self.breaks.iter().enumerate() {
219 let mapped = self.marker_to_idx.get(&b.marker_id).copied();
220 assert_eq!(
221 mapped,
222 Some(i),
223 "marker {:?} should map to idx {} but maps to {:?}",
224 b.marker_id,
225 i,
226 mapped
227 );
228 }
229 }
230}
231
232impl Default for SoftBreakManager {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn ns() -> OverlayNamespace {
243 OverlayNamespace::from_string("test".to_string())
244 }
245
246 #[test]
247 fn test_soft_break_remove_in_range_keeps_only_outside() {
248 let mut marker_list = MarkerList::new();
249 marker_list.set_buffer_size(200);
250 let mut manager = SoftBreakManager::new();
251
252 manager.add(&mut marker_list, ns(), 5, 0);
253 manager.add(&mut marker_list, ns(), 25, 0);
254 manager.add(&mut marker_list, ns(), 45, 0);
255 manager.add(&mut marker_list, ns(), 65, 0);
256
257 manager.remove_in_range(20, 50, &mut marker_list);
259
260 let kept: Vec<_> = manager
261 .query_viewport(0, 1000, &marker_list)
262 .into_iter()
263 .map(|(p, _)| p)
264 .collect();
265 assert_eq!(kept, vec![5, 65]);
266 }
267
268 #[test]
269 fn test_soft_break_remove_in_range_endpoint_semantics() {
270 let mut marker_list = MarkerList::new();
272 marker_list.set_buffer_size(100);
273 let mut manager = SoftBreakManager::new();
274
275 manager.add(&mut marker_list, ns(), 10, 0);
276 manager.add(&mut marker_list, ns(), 20, 0);
277
278 manager.remove_in_range(10, 20, &mut marker_list);
279 let kept: Vec<_> = manager
280 .query_viewport(0, 1000, &marker_list)
281 .into_iter()
282 .map(|(p, _)| p)
283 .collect();
284 assert_eq!(kept, vec![20]);
285 }
286
287 #[test]
288 fn test_soft_break_remove_in_range_bumps_version_only_on_change() {
289 let mut marker_list = MarkerList::new();
290 marker_list.set_buffer_size(100);
291 let mut manager = SoftBreakManager::new();
292
293 manager.add(&mut marker_list, ns(), 10, 0);
294 let v0 = manager.version();
295
296 manager.remove_in_range(50, 60, &mut marker_list);
297 assert_eq!(manager.version(), v0);
298
299 manager.remove_in_range(0, 50, &mut marker_list);
300 assert!(manager.is_empty());
301 assert_ne!(manager.version(), v0);
302 }
303
304 #[test]
313 fn perf_full_buffer_rebuild_pass() {
314 const LINES: usize = 500;
315 const LINE_BYTES: usize = 50;
316 const BREAKS_PER_LINE: usize = 5;
317
318 let mut marker_list = MarkerList::new();
319 marker_list.set_buffer_size(LINES * LINE_BYTES);
320 let mut manager = SoftBreakManager::new();
321
322 let break_byte = |line: usize, k: usize| -> usize {
323 line * LINE_BYTES + k * (LINE_BYTES / BREAKS_PER_LINE)
324 };
325
326 for line in 0..LINES {
328 for k in 0..BREAKS_PER_LINE {
329 manager.add(&mut marker_list, ns(), break_byte(line, k), 0);
330 }
331 }
332 let initial = LINES * BREAKS_PER_LINE;
333
334 let start = std::time::Instant::now();
336 for line in 0..LINES {
337 let line_start = line * LINE_BYTES;
338 let line_end = line_start + LINE_BYTES;
339 manager.remove_in_range(line_start, line_end, &mut marker_list);
340 for k in 0..BREAKS_PER_LINE {
341 manager.add(&mut marker_list, ns(), break_byte(line, k), 0);
342 }
343 }
344 let elapsed = start.elapsed();
345
346 eprintln!(
347 "[perf] soft_break full-buffer rebuild ({LINES} lines, {} entries steady): \
348 {:?} total, {:?}/line",
349 initial,
350 elapsed,
351 elapsed / LINES as u32,
352 );
353 let still_present = manager
354 .query_viewport(0, LINES * LINE_BYTES, &marker_list)
355 .len();
356 assert_eq!(still_present, initial);
357 }
358
359 mod proptests {
360 use super::*;
361 use proptest::prelude::*;
362
363 #[derive(Debug, Clone)]
364 enum Op {
365 Add { pos: usize, indent: u16, ns_idx: u8 },
366 RemoveInRange { start: usize, end: usize },
367 ClearNamespace { ns_idx: u8 },
368 }
369
370 const BUFFER_SIZE: usize = 200;
371
372 fn arb_op() -> impl Strategy<Value = Op> {
373 prop_oneof![
374 3 => (0..BUFFER_SIZE, 0u16..8u16, 0u8..3u8)
375 .prop_map(|(pos, indent, ns_idx)| Op::Add { pos, indent, ns_idx }),
376 2 => (0..BUFFER_SIZE, 0..BUFFER_SIZE)
377 .prop_map(|(a, b)| {
378 let (s, e) = if a <= b { (a, b) } else { (b, a) };
379 Op::RemoveInRange { start: s, end: e }
380 }),
381 1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
382 ]
383 }
384
385 fn nsf(idx: u8) -> OverlayNamespace {
386 OverlayNamespace::from_string(format!("ns{idx}"))
387 }
388
389 proptest! {
390 #[test]
394 fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..40)) {
395 let mut marker_list = MarkerList::new();
396 marker_list.set_buffer_size(BUFFER_SIZE);
397 let mut manager = SoftBreakManager::new();
398
399 for op in ops {
400 match op {
401 Op::Add { pos, indent, ns_idx } => {
402 manager.add(&mut marker_list, nsf(ns_idx), pos, indent);
403 }
404 Op::RemoveInRange { start, end } => {
405 manager.remove_in_range(start, end, &mut marker_list);
406 for (p, _) in manager.query_viewport(0, BUFFER_SIZE, &marker_list) {
408 prop_assert!(
409 !(p >= start && p < end),
410 "entry at {p} survived remove_in_range({start}..{end})",
411 );
412 }
413 }
414 Op::ClearNamespace { ns_idx } => {
415 manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
416 }
417 }
418 manager.check_invariants();
419 }
420 }
421 }
422 }
423}