1use crate::state::EditorState;
39use crate::view::line_wrap_cache::{
40 count_visual_rows_for_text, count_visual_rows_for_text_with_soft_breaks,
41 pipeline_inputs_version, CacheViewMode, LineWrapKey, WrapGeometry,
42};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct VisualRowIndexKey {
50 pub pipeline_inputs_version: u64,
51 pub view_mode: CacheViewMode,
52 pub effective_width: u32,
53 pub gutter_width: u16,
54 pub wrap_column: Option<u32>,
55 pub hanging_indent: bool,
56 pub line_wrap_enabled: bool,
57}
58
59impl VisualRowIndexKey {
60 fn line_key(&self, line_start: usize) -> LineWrapKey {
62 LineWrapKey {
63 pipeline_inputs_version: self.pipeline_inputs_version,
64 view_mode: self.view_mode,
65 line_start,
66 effective_width: self.effective_width,
67 gutter_width: self.gutter_width,
68 wrap_column: self.wrap_column,
69 hanging_indent: self.hanging_indent,
70 line_wrap_enabled: self.line_wrap_enabled,
71 }
72 }
73
74 fn geom(&self) -> WrapGeometry {
75 WrapGeometry {
76 effective_width: self.effective_width as usize,
77 gutter_width: self.gutter_width as usize,
78 hanging_indent: self.hanging_indent,
79 wrap_column: self.wrap_column,
80 line_wrap_enabled: self.line_wrap_enabled,
81 view_mode: self.view_mode,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Default)]
89pub struct VisualRowIndex {
90 key: Option<VisualRowIndexKey>,
91 prefix_sums: Vec<u32>,
95 line_starts: Vec<usize>,
99}
100
101impl VisualRowIndex {
102 pub fn is_built_for(&self, key: &VisualRowIndexKey) -> bool {
103 self.key.as_ref() == Some(key)
104 }
105
106 pub fn clear(&mut self) {
110 self.key = None;
111 self.prefix_sums.clear();
112 self.line_starts.clear();
113 }
114
115 pub fn line_count(&self) -> usize {
117 self.prefix_sums.len().saturating_sub(1)
118 }
119
120 pub fn total_rows(&self) -> u32 {
122 *self.prefix_sums.last().unwrap_or(&0)
123 }
124
125 pub fn line_first_row(&self, line_idx: usize) -> u32 {
127 *self.prefix_sums.get(line_idx).unwrap_or(&self.total_rows())
128 }
129
130 pub fn line_row_count(&self, line_idx: usize) -> u32 {
132 let next = self
133 .prefix_sums
134 .get(line_idx + 1)
135 .copied()
136 .unwrap_or_else(|| self.total_rows());
137 next - self.line_first_row(line_idx)
138 }
139
140 pub fn line_start_byte(&self, line_idx: usize) -> usize {
142 *self.line_starts.get(line_idx).unwrap_or(&0)
143 }
144
145 pub fn line_for_byte(&self, byte: usize) -> (usize, usize) {
149 let n = self.line_count();
150 if n == 0 {
151 return (0, 0);
152 }
153 let p = self.line_starts.partition_point(|&s| s <= byte);
157 let i = p.saturating_sub(1).min(n - 1);
158 (i, self.line_starts[i])
159 }
160
161 pub fn position_at_row(&self, row: u32) -> (usize, usize, usize) {
165 if self.prefix_sums.is_empty() {
166 return (0, 0, 0);
167 }
168 let total = self.total_rows();
169 let target = row.min(total.saturating_sub(1));
170 let p = self.prefix_sums.partition_point(|&s| s <= target);
172 let i = p.saturating_sub(1).min(self.line_count().saturating_sub(1));
173 let offset = (target - self.prefix_sums[i]) as usize;
174 (i, self.line_starts[i], offset)
175 }
176}
177
178pub fn ensure_built(state: &mut EditorState, key: &VisualRowIndexKey) {
198 if state.visual_row_index.is_built_for(key) {
199 return;
200 }
201
202 let buffer_len = state.buffer.len();
203 let line_count = state
204 .buffer
205 .line_count()
206 .unwrap_or_else(|| (buffer_len / state.buffer.estimated_line_length()).max(1));
207 if line_count == 0 {
208 state.visual_row_index = VisualRowIndex {
211 key: Some(*key),
212 prefix_sums: vec![0],
213 line_starts: vec![0],
214 };
215 return;
216 }
217
218 let effective_width = key.effective_width as usize;
219 let gutter_width = key.gutter_width as usize;
220 let hanging_indent = key.hanging_indent;
221
222 let soft_break_pairs: Vec<(usize, u16)> = if state.soft_breaks.is_empty() {
234 Vec::new()
235 } else {
236 state
237 .soft_breaks
238 .query_viewport(0, buffer_len + 1, &state.marker_list)
239 };
240 let virtual_line_positions: Vec<usize> = if state.virtual_texts.is_empty() {
241 Vec::new()
242 } else {
243 let mut v: Vec<usize> = state
244 .virtual_texts
245 .query_lines_in_range(&state.marker_list, 0, buffer_len + 1)
246 .into_iter()
247 .map(|(pos, _)| pos)
248 .collect();
249 v.sort_unstable();
250 v
251 };
252
253 let mut prefix_sums: Vec<u32> = Vec::with_capacity(line_count + 1);
256 let mut line_starts: Vec<usize> = Vec::with_capacity(line_count + 1);
257 let mut running: u32 = 0;
258 prefix_sums.push(0);
259
260 for line_idx in 0..line_count {
261 let line_start = state
262 .buffer
263 .line_start_offset(line_idx)
264 .unwrap_or(buffer_len);
265 let line_end = if line_idx + 1 < line_count {
266 state
267 .buffer
268 .line_start_offset(line_idx + 1)
269 .unwrap_or(buffer_len)
270 } else {
271 buffer_len
272 };
273 line_starts.push(line_start);
274
275 let line_breaks = slice_in_range(&soft_break_pairs, line_start, line_end);
276 let virtual_rows = count_in_range(&virtual_line_positions, line_start, line_end) as u32;
277
278 let line_key = key.line_key(line_start);
279 let wrap_rows: u32 = if let Some(cached) = state.line_wrap_cache.get(&line_key) {
280 (cached.len() as u32).max(1)
286 } else if !key.line_wrap_enabled {
287 1
290 } else {
291 let Some(bytes) = state.buffer.get_line(line_idx) else {
300 running = running.saturating_add(1 + virtual_rows);
302 prefix_sums.push(running);
303 continue;
304 };
305 let line_content = String::from_utf8_lossy(&bytes);
306 let trimmed = line_content.trim_end_matches('\n').trim_end_matches('\r');
307 if line_breaks.is_empty() {
308 count_visual_rows_for_text(trimmed, effective_width, gutter_width, hanging_indent)
309 } else {
310 count_visual_rows_for_text_with_soft_breaks(
311 trimmed,
312 line_start,
313 line_breaks,
314 effective_width,
315 gutter_width,
316 hanging_indent,
317 )
318 }
319 };
320
321 running = running.saturating_add(wrap_rows.saturating_add(virtual_rows));
322 prefix_sums.push(running);
323 }
324 line_starts.push(buffer_len);
325
326 state.visual_row_index = VisualRowIndex {
327 key: Some(*key),
328 prefix_sums,
329 line_starts,
330 };
331}
332
333fn slice_in_range(pairs: &[(usize, u16)], start: usize, end: usize) -> &[(usize, u16)] {
336 let lo = pairs.partition_point(|(p, _)| *p < start);
337 let hi = pairs.partition_point(|(p, _)| *p < end);
338 &pairs[lo..hi]
339}
340
341fn count_in_range(positions: &[usize], start: usize, end: usize) -> usize {
343 let lo = positions.partition_point(|p| *p < start);
344 let hi = positions.partition_point(|p| *p < end);
345 hi - lo
346}
347
348pub fn ensure_built_from_geom(state: &mut EditorState, geom: &WrapGeometry) {
351 let key = VisualRowIndexKey {
352 pipeline_inputs_version: pipeline_inputs_version(
353 state.buffer.version(),
354 state.soft_breaks.version(),
355 state.conceals.version(),
356 state.virtual_texts.version(),
357 ),
358 view_mode: geom.view_mode,
359 effective_width: geom.effective_width as u32,
360 gutter_width: geom.gutter_width as u16,
361 wrap_column: geom.wrap_column,
362 hanging_indent: geom.hanging_indent,
363 line_wrap_enabled: geom.line_wrap_enabled,
364 };
365 ensure_built(state, &key);
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 fn idx_with(prefix: Vec<u32>, starts: Vec<usize>) -> VisualRowIndex {
373 VisualRowIndex {
374 key: Some(VisualRowIndexKey {
375 pipeline_inputs_version: 0,
376 view_mode: CacheViewMode::Source,
377 effective_width: 80,
378 gutter_width: 6,
379 wrap_column: None,
380 hanging_indent: false,
381 line_wrap_enabled: true,
382 }),
383 prefix_sums: prefix,
384 line_starts: starts,
385 }
386 }
387
388 #[test]
389 fn empty_index_total_is_zero() {
390 let idx = VisualRowIndex::default();
391 assert_eq!(idx.total_rows(), 0);
392 assert_eq!(idx.line_count(), 0);
393 }
394
395 #[test]
396 fn single_line_one_row() {
397 let idx = idx_with(vec![0, 1], vec![0, 10]);
398 assert_eq!(idx.total_rows(), 1);
399 assert_eq!(idx.line_count(), 1);
400 assert_eq!(idx.line_first_row(0), 0);
401 assert_eq!(idx.line_row_count(0), 1);
402 assert_eq!(idx.position_at_row(0), (0, 0, 0));
403 }
404
405 #[test]
406 fn multi_line_no_wrap() {
407 let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
409 assert_eq!(idx.total_rows(), 3);
410 assert_eq!(idx.line_count(), 3);
411 assert_eq!(idx.position_at_row(0), (0, 0, 0));
412 assert_eq!(idx.position_at_row(1), (1, 10, 0));
413 assert_eq!(idx.position_at_row(2), (2, 20, 0));
414 assert_eq!(idx.position_at_row(99), (2, 20, 0));
416 }
417
418 #[test]
419 fn wrapped_line_offsets() {
420 let idx = idx_with(vec![0, 1, 4, 6], vec![0, 10, 200, 300]);
422 assert_eq!(idx.total_rows(), 6);
423 assert_eq!(idx.line_row_count(0), 1);
424 assert_eq!(idx.line_row_count(1), 3);
425 assert_eq!(idx.line_row_count(2), 2);
426 assert_eq!(idx.position_at_row(0), (0, 0, 0));
428 assert_eq!(idx.position_at_row(1), (1, 10, 0));
430 assert_eq!(idx.position_at_row(2), (1, 10, 1));
431 assert_eq!(idx.position_at_row(3), (1, 10, 2));
432 assert_eq!(idx.position_at_row(4), (2, 200, 0));
434 assert_eq!(idx.position_at_row(5), (2, 200, 1));
435 }
436
437 #[test]
438 fn line_for_byte_resolves_to_containing_line() {
439 let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
440 assert_eq!(idx.line_for_byte(0), (0, 0));
441 assert_eq!(idx.line_for_byte(5), (0, 0));
442 assert_eq!(idx.line_for_byte(10), (1, 10));
443 assert_eq!(idx.line_for_byte(15), (1, 10));
444 assert_eq!(idx.line_for_byte(20), (2, 20));
445 assert_eq!(idx.line_for_byte(29), (2, 20));
446 assert_eq!(idx.line_for_byte(99), (2, 20));
448 }
449
450 #[test]
451 fn is_built_for_detects_key_mismatch() {
452 let idx = idx_with(vec![0, 1], vec![0, 10]);
453 let mut k = idx.key.unwrap();
454 assert!(idx.is_built_for(&k));
455 k.effective_width += 1;
456 assert!(!idx.is_built_for(&k));
457 }
458
459 #[test]
460 fn clear_resets_to_default() {
461 let mut idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
462 idx.clear();
463 assert_eq!(idx.total_rows(), 0);
464 assert_eq!(idx.line_count(), 0);
465 assert!(idx.key.is_none());
466 }
467}