use crate::state::EditorState;
use crate::view::line_wrap_cache::{
count_visual_rows_for_text, count_visual_rows_for_text_with_soft_breaks,
pipeline_inputs_version, CacheViewMode, LineWrapKey, WrapGeometry,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VisualRowIndexKey {
pub pipeline_inputs_version: u64,
pub view_mode: CacheViewMode,
pub effective_width: u32,
pub gutter_width: u16,
pub wrap_column: Option<u32>,
pub hanging_indent: bool,
pub line_wrap_enabled: bool,
}
impl VisualRowIndexKey {
fn line_key(&self, line_start: usize) -> LineWrapKey {
LineWrapKey {
pipeline_inputs_version: self.pipeline_inputs_version,
view_mode: self.view_mode,
line_start,
effective_width: self.effective_width,
gutter_width: self.gutter_width,
wrap_column: self.wrap_column,
hanging_indent: self.hanging_indent,
line_wrap_enabled: self.line_wrap_enabled,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct VisualRowIndex {
key: Option<VisualRowIndexKey>,
prefix_sums: Vec<u32>,
line_starts: Vec<usize>,
}
impl VisualRowIndex {
pub fn is_built_for(&self, key: &VisualRowIndexKey) -> bool {
self.key.as_ref() == Some(key)
}
pub fn clear(&mut self) {
self.key = None;
self.prefix_sums.clear();
self.line_starts.clear();
}
pub fn line_count(&self) -> usize {
self.prefix_sums.len().saturating_sub(1)
}
pub fn total_rows(&self) -> u32 {
*self.prefix_sums.last().unwrap_or(&0)
}
pub fn line_first_row(&self, line_idx: usize) -> u32 {
*self.prefix_sums.get(line_idx).unwrap_or(&self.total_rows())
}
pub fn line_row_count(&self, line_idx: usize) -> u32 {
let next = self
.prefix_sums
.get(line_idx + 1)
.copied()
.unwrap_or_else(|| self.total_rows());
next - self.line_first_row(line_idx)
}
pub fn line_start_byte(&self, line_idx: usize) -> usize {
*self.line_starts.get(line_idx).unwrap_or(&0)
}
pub fn line_for_byte(&self, byte: usize) -> (usize, usize) {
let n = self.line_count();
if n == 0 {
return (0, 0);
}
let p = self.line_starts.partition_point(|&s| s <= byte);
let i = p.saturating_sub(1).min(n - 1);
(i, self.line_starts[i])
}
pub fn position_at_row(&self, row: u32) -> (usize, usize, usize) {
if self.prefix_sums.is_empty() {
return (0, 0, 0);
}
let total = self.total_rows();
let target = row.min(total.saturating_sub(1));
let p = self.prefix_sums.partition_point(|&s| s <= target);
let i = p.saturating_sub(1).min(self.line_count().saturating_sub(1));
let offset = (target - self.prefix_sums[i]) as usize;
(i, self.line_starts[i], offset)
}
}
pub fn ensure_built(state: &mut EditorState, key: &VisualRowIndexKey) {
if state.visual_row_index.is_built_for(key) {
return;
}
let buffer_len = state.buffer.len();
let line_count = state
.buffer
.line_count()
.unwrap_or_else(|| (buffer_len / state.buffer.estimated_line_length()).max(1));
if line_count == 0 {
state.visual_row_index = VisualRowIndex {
key: Some(*key),
prefix_sums: vec![0],
line_starts: vec![0],
};
return;
}
let effective_width = key.effective_width as usize;
let gutter_width = key.gutter_width as usize;
let hanging_indent = key.hanging_indent;
let soft_break_pairs: Vec<(usize, u16)> = if state.soft_breaks.is_empty() {
Vec::new()
} else {
state
.soft_breaks
.query_viewport(0, buffer_len + 1, &state.marker_list)
};
let virtual_line_positions: Vec<usize> = if state.virtual_texts.is_empty() {
Vec::new()
} else {
let mut v: Vec<usize> = state
.virtual_texts
.query_lines_in_range(&state.marker_list, 0, buffer_len + 1)
.into_iter()
.map(|(pos, _)| pos)
.collect();
v.sort_unstable();
v
};
let mut prefix_sums: Vec<u32> = Vec::with_capacity(line_count + 1);
let mut line_starts: Vec<usize> = Vec::with_capacity(line_count + 1);
let mut running: u32 = 0;
prefix_sums.push(0);
for line_idx in 0..line_count {
let line_start = state
.buffer
.line_start_offset(line_idx)
.unwrap_or(buffer_len);
let line_end = if line_idx + 1 < line_count {
state
.buffer
.line_start_offset(line_idx + 1)
.unwrap_or(buffer_len)
} else {
buffer_len
};
line_starts.push(line_start);
let line_breaks = slice_in_range(&soft_break_pairs, line_start, line_end);
let virtual_rows = count_in_range(&virtual_line_positions, line_start, line_end) as u32;
let line_key = key.line_key(line_start);
let wrap_rows: u32 = if let Some(cached) = state.line_wrap_cache.get(&line_key) {
(cached.len() as u32).max(1)
} else if !key.line_wrap_enabled {
1
} else {
let Some(bytes) = state.buffer.get_line(line_idx) else {
running = running.saturating_add(1 + virtual_rows);
prefix_sums.push(running);
continue;
};
let line_content = String::from_utf8_lossy(&bytes);
let trimmed = line_content.trim_end_matches('\n').trim_end_matches('\r');
if line_breaks.is_empty() {
count_visual_rows_for_text(trimmed, effective_width, gutter_width, hanging_indent)
} else {
count_visual_rows_for_text_with_soft_breaks(
trimmed,
line_start,
line_breaks,
effective_width,
gutter_width,
hanging_indent,
)
}
};
running = running.saturating_add(wrap_rows.saturating_add(virtual_rows));
prefix_sums.push(running);
}
line_starts.push(buffer_len);
state.visual_row_index = VisualRowIndex {
key: Some(*key),
prefix_sums,
line_starts,
};
}
fn slice_in_range(pairs: &[(usize, u16)], start: usize, end: usize) -> &[(usize, u16)] {
let lo = pairs.partition_point(|(p, _)| *p < start);
let hi = pairs.partition_point(|(p, _)| *p < end);
&pairs[lo..hi]
}
fn count_in_range(positions: &[usize], start: usize, end: usize) -> usize {
let lo = positions.partition_point(|p| *p < start);
let hi = positions.partition_point(|p| *p < end);
hi - lo
}
pub fn ensure_built_from_geom(state: &mut EditorState, geom: &WrapGeometry) {
let key = VisualRowIndexKey {
pipeline_inputs_version: pipeline_inputs_version(
state.buffer.version(),
state.soft_breaks.version(),
state.conceals.version(),
state.virtual_texts.version(),
),
view_mode: geom.view_mode,
effective_width: geom.effective_width as u32,
gutter_width: geom.gutter_width as u16,
wrap_column: geom.wrap_column,
hanging_indent: geom.hanging_indent,
line_wrap_enabled: geom.line_wrap_enabled,
};
ensure_built(state, &key);
}
#[cfg(test)]
mod tests {
use super::*;
fn idx_with(prefix: Vec<u32>, starts: Vec<usize>) -> VisualRowIndex {
VisualRowIndex {
key: Some(VisualRowIndexKey {
pipeline_inputs_version: 0,
view_mode: CacheViewMode::Source,
effective_width: 80,
gutter_width: 6,
wrap_column: None,
hanging_indent: false,
line_wrap_enabled: true,
}),
prefix_sums: prefix,
line_starts: starts,
}
}
#[test]
fn empty_index_total_is_zero() {
let idx = VisualRowIndex::default();
assert_eq!(idx.total_rows(), 0);
assert_eq!(idx.line_count(), 0);
}
#[test]
fn single_line_one_row() {
let idx = idx_with(vec![0, 1], vec![0, 10]);
assert_eq!(idx.total_rows(), 1);
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.line_first_row(0), 0);
assert_eq!(idx.line_row_count(0), 1);
assert_eq!(idx.position_at_row(0), (0, 0, 0));
}
#[test]
fn multi_line_no_wrap() {
let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
assert_eq!(idx.total_rows(), 3);
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.position_at_row(0), (0, 0, 0));
assert_eq!(idx.position_at_row(1), (1, 10, 0));
assert_eq!(idx.position_at_row(2), (2, 20, 0));
assert_eq!(idx.position_at_row(99), (2, 20, 0));
}
#[test]
fn wrapped_line_offsets() {
let idx = idx_with(vec![0, 1, 4, 6], vec![0, 10, 200, 300]);
assert_eq!(idx.total_rows(), 6);
assert_eq!(idx.line_row_count(0), 1);
assert_eq!(idx.line_row_count(1), 3);
assert_eq!(idx.line_row_count(2), 2);
assert_eq!(idx.position_at_row(0), (0, 0, 0));
assert_eq!(idx.position_at_row(1), (1, 10, 0));
assert_eq!(idx.position_at_row(2), (1, 10, 1));
assert_eq!(idx.position_at_row(3), (1, 10, 2));
assert_eq!(idx.position_at_row(4), (2, 200, 0));
assert_eq!(idx.position_at_row(5), (2, 200, 1));
}
#[test]
fn line_for_byte_resolves_to_containing_line() {
let idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
assert_eq!(idx.line_for_byte(0), (0, 0));
assert_eq!(idx.line_for_byte(5), (0, 0));
assert_eq!(idx.line_for_byte(10), (1, 10));
assert_eq!(idx.line_for_byte(15), (1, 10));
assert_eq!(idx.line_for_byte(20), (2, 20));
assert_eq!(idx.line_for_byte(29), (2, 20));
assert_eq!(idx.line_for_byte(99), (2, 20));
}
#[test]
fn is_built_for_detects_key_mismatch() {
let idx = idx_with(vec![0, 1], vec![0, 10]);
let mut k = idx.key.unwrap();
assert!(idx.is_built_for(&k));
k.effective_width += 1;
assert!(!idx.is_built_for(&k));
}
#[test]
fn clear_resets_to_default() {
let mut idx = idx_with(vec![0, 1, 2, 3], vec![0, 10, 20, 30]);
idx.clear();
assert_eq!(idx.total_rows(), 0);
assert_eq!(idx.line_count(), 0);
assert!(idx.key.is_none());
}
}