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
75#[derive(Debug, Clone, Default)]
78pub struct VisualRowIndex {
79 key: Option<VisualRowIndexKey>,
80 prefix_sums: Vec<u32>,
84 line_starts: Vec<usize>,
88}
89
90impl VisualRowIndex {
91 pub fn is_built_for(&self, key: &VisualRowIndexKey) -> bool {
92 self.key.as_ref() == Some(key)
93 }
94
95 pub fn clear(&mut self) {
99 self.key = None;
100 self.prefix_sums.clear();
101 self.line_starts.clear();
102 }
103
104 pub fn line_count(&self) -> usize {
106 self.prefix_sums.len().saturating_sub(1)
107 }
108
109 pub fn total_rows(&self) -> u32 {
111 *self.prefix_sums.last().unwrap_or(&0)
112 }
113
114 pub fn line_first_row(&self, line_idx: usize) -> u32 {
116 *self.prefix_sums.get(line_idx).unwrap_or(&self.total_rows())
117 }
118
119 pub fn line_row_count(&self, line_idx: usize) -> u32 {
121 let next = self
122 .prefix_sums
123 .get(line_idx + 1)
124 .copied()
125 .unwrap_or_else(|| self.total_rows());
126 next - self.line_first_row(line_idx)
127 }
128
129 pub fn line_start_byte(&self, line_idx: usize) -> usize {
131 *self.line_starts.get(line_idx).unwrap_or(&0)
132 }
133
134 pub fn line_for_byte(&self, byte: usize) -> (usize, usize) {
138 let n = self.line_count();
139 if n == 0 {
140 return (0, 0);
141 }
142 let p = self.line_starts.partition_point(|&s| s <= byte);
146 let i = p.saturating_sub(1).min(n - 1);
147 (i, self.line_starts[i])
148 }
149
150 pub fn position_at_row(&self, row: u32) -> (usize, usize, usize) {
154 if self.prefix_sums.is_empty() {
155 return (0, 0, 0);
156 }
157 let total = self.total_rows();
158 let target = row.min(total.saturating_sub(1));
159 let p = self.prefix_sums.partition_point(|&s| s <= target);
161 let i = p.saturating_sub(1).min(self.line_count().saturating_sub(1));
162 let offset = (target - self.prefix_sums[i]) as usize;
163 (i, self.line_starts[i], offset)
164 }
165}
166
167pub fn ensure_built(state: &mut EditorState, key: &VisualRowIndexKey) {
187 if state.visual_row_index.is_built_for(key) {
188 return;
189 }
190
191 let buffer_len = state.buffer.len();
192 let line_count = state
193 .buffer
194 .line_count()
195 .unwrap_or_else(|| (buffer_len / state.buffer.estimated_line_length()).max(1));
196 if line_count == 0 {
197 state.visual_row_index = VisualRowIndex {
200 key: Some(*key),
201 prefix_sums: vec![0],
202 line_starts: vec![0],
203 };
204 return;
205 }
206
207 let effective_width = key.effective_width as usize;
208 let gutter_width = key.gutter_width as usize;
209 let hanging_indent = key.hanging_indent;
210
211 let soft_break_pairs: Vec<(usize, u16)> = if state.soft_breaks.is_empty() {
223 Vec::new()
224 } else {
225 state
226 .soft_breaks
227 .query_viewport(0, buffer_len + 1, &state.marker_list)
228 };
229 let virtual_line_positions: Vec<usize> = if state.virtual_texts.is_empty() {
230 Vec::new()
231 } else {
232 let mut v: Vec<usize> = state
233 .virtual_texts
234 .query_lines_in_range(&state.marker_list, 0, buffer_len + 1)
235 .into_iter()
236 .map(|(pos, _)| pos)
237 .collect();
238 v.sort_unstable();
239 v
240 };
241
242 let mut prefix_sums: Vec<u32> = Vec::with_capacity(line_count + 1);
245 let mut line_starts: Vec<usize> = Vec::with_capacity(line_count + 1);
246 let mut running: u32 = 0;
247 prefix_sums.push(0);
248
249 for line_idx in 0..line_count {
250 let line_start = state
251 .buffer
252 .line_start_offset(line_idx)
253 .unwrap_or(buffer_len);
254 let line_end = if line_idx + 1 < line_count {
255 state
256 .buffer
257 .line_start_offset(line_idx + 1)
258 .unwrap_or(buffer_len)
259 } else {
260 buffer_len
261 };
262 line_starts.push(line_start);
263
264 let line_breaks = slice_in_range(&soft_break_pairs, line_start, line_end);
265 let virtual_rows = count_in_range(&virtual_line_positions, line_start, line_end) as u32;
266
267 let line_key = key.line_key(line_start);
268 let wrap_rows: u32 = if let Some(cached) = state.line_wrap_cache.get(&line_key) {
269 (cached.len() as u32).max(1)
275 } else if !key.line_wrap_enabled {
276 1
279 } else {
280 let Some(bytes) = state.buffer.get_line(line_idx) else {
289 running = running.saturating_add(1 + virtual_rows);
291 prefix_sums.push(running);
292 continue;
293 };
294 let line_content = String::from_utf8_lossy(&bytes);
295 let trimmed = line_content.trim_end_matches('\n').trim_end_matches('\r');
296 if line_breaks.is_empty() {
297 count_visual_rows_for_text(trimmed, effective_width, gutter_width, hanging_indent)
298 } else {
299 count_visual_rows_for_text_with_soft_breaks(
300 trimmed,
301 line_start,
302 line_breaks,
303 effective_width,
304 gutter_width,
305 hanging_indent,
306 )
307 }
308 };
309
310 running = running.saturating_add(wrap_rows.saturating_add(virtual_rows));
311 prefix_sums.push(running);
312 }
313 line_starts.push(buffer_len);
314
315 state.visual_row_index = VisualRowIndex {
316 key: Some(*key),
317 prefix_sums,
318 line_starts,
319 };
320}
321
322fn slice_in_range(pairs: &[(usize, u16)], start: usize, end: usize) -> &[(usize, u16)] {
325 let lo = pairs.partition_point(|(p, _)| *p < start);
326 let hi = pairs.partition_point(|(p, _)| *p < end);
327 &pairs[lo..hi]
328}
329
330fn count_in_range(positions: &[usize], start: usize, end: usize) -> usize {
332 let lo = positions.partition_point(|p| *p < start);
333 let hi = positions.partition_point(|p| *p < end);
334 hi - lo
335}
336
337pub fn ensure_built_from_geom(state: &mut EditorState, geom: &WrapGeometry) {
340 let key = VisualRowIndexKey {
341 pipeline_inputs_version: pipeline_inputs_version(
342 state.buffer.version(),
343 state.soft_breaks.version(),
344 state.conceals.version(),
345 state.virtual_texts.version(),
346 ),
347 view_mode: geom.view_mode,
348 effective_width: geom.effective_width as u32,
349 gutter_width: geom.gutter_width as u16,
350 wrap_column: geom.wrap_column,
351 hanging_indent: geom.hanging_indent,
352 line_wrap_enabled: geom.line_wrap_enabled,
353 };
354 ensure_built(state, &key);
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn idx_with(prefix: Vec<u32>, starts: Vec<usize>) -> VisualRowIndex {
362 VisualRowIndex {
363 key: Some(VisualRowIndexKey {
364 pipeline_inputs_version: 0,
365 view_mode: CacheViewMode::Source,
366 effective_width: 80,
367 gutter_width: 6,
368 wrap_column: None,
369 hanging_indent: false,
370 line_wrap_enabled: true,
371 }),
372 prefix_sums: prefix,
373 line_starts: starts,
374 }
375 }
376
377 #[test]
378 fn empty_index_total_is_zero() {
379 let idx = VisualRowIndex::default();
380 assert_eq!(idx.total_rows(), 0);
381 assert_eq!(idx.line_count(), 0);
382 }
383
384 #[test]
385 fn single_line_one_row() {
386 let idx = idx_with(vec![0, 1], vec![0, 10]);
387 assert_eq!(idx.total_rows(), 1);
388 assert_eq!(idx.line_count(), 1);
389 assert_eq!(idx.line_first_row(0), 0);
390 assert_eq!(idx.line_row_count(0), 1);
391 assert_eq!(idx.position_at_row(0), (0, 0, 0));
392 }
393
394 #[test]
395 fn multi_line_no_wrap() {
396 let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
398 assert_eq!(idx.total_rows(), 3);
399 assert_eq!(idx.line_count(), 3);
400 assert_eq!(idx.position_at_row(0), (0, 0, 0));
401 assert_eq!(idx.position_at_row(1), (1, 10, 0));
402 assert_eq!(idx.position_at_row(2), (2, 20, 0));
403 assert_eq!(idx.position_at_row(99), (2, 20, 0));
405 }
406
407 #[test]
408 fn wrapped_line_offsets() {
409 let idx = idx_with(vec![0, 1, 4, 6], vec![0, 10, 200, 300]);
411 assert_eq!(idx.total_rows(), 6);
412 assert_eq!(idx.line_row_count(0), 1);
413 assert_eq!(idx.line_row_count(1), 3);
414 assert_eq!(idx.line_row_count(2), 2);
415 assert_eq!(idx.position_at_row(0), (0, 0, 0));
417 assert_eq!(idx.position_at_row(1), (1, 10, 0));
419 assert_eq!(idx.position_at_row(2), (1, 10, 1));
420 assert_eq!(idx.position_at_row(3), (1, 10, 2));
421 assert_eq!(idx.position_at_row(4), (2, 200, 0));
423 assert_eq!(idx.position_at_row(5), (2, 200, 1));
424 }
425
426 #[test]
427 fn line_for_byte_resolves_to_containing_line() {
428 let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
429 assert_eq!(idx.line_for_byte(0), (0, 0));
430 assert_eq!(idx.line_for_byte(5), (0, 0));
431 assert_eq!(idx.line_for_byte(10), (1, 10));
432 assert_eq!(idx.line_for_byte(15), (1, 10));
433 assert_eq!(idx.line_for_byte(20), (2, 20));
434 assert_eq!(idx.line_for_byte(29), (2, 20));
435 assert_eq!(idx.line_for_byte(99), (2, 20));
437 }
438
439 #[test]
440 fn is_built_for_detects_key_mismatch() {
441 let idx = idx_with(vec![0, 1], vec![0, 10]);
442 let mut k = idx.key.unwrap();
443 assert!(idx.is_built_for(&k));
444 k.effective_width += 1;
445 assert!(!idx.is_built_for(&k));
446 }
447
448 #[test]
449 fn clear_resets_to_default() {
450 let mut idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
451 idx.clear();
452 assert_eq!(idx.total_rows(), 0);
453 assert_eq!(idx.line_count(), 0);
454 assert!(idx.key.is_none());
455 }
456}