/// WrapMap: Soft-wrapping layer (Buffer → Wrap rows). /// /// This module wraps the existing TextWrapper and provides: /// - BufferPoint ↔ WrapPoint mapping /// - Efficient buffer_line → wrap_row queries via prefix sum cache /// - Incremental updates when text or layout changes use std::ops::Range; use gpui::{App, Font, Pixels}; use ropey::Rope; use super::text_wrapper::{LineItem, TextWrapper}; /// WrapMap manages soft-wrapping and provides buffer ↔ wrap coordinate mapping. pub struct WrapMap { /// The underlying text wrapper (reuses existing implementation) wrapper: TextWrapper, /// Prefix sum cache: buffer_line_starts[line] = first wrap_row for buffer line `line` /// This allows O(1) lookup of buffer_line → wrap_row buffer_line_starts: Vec, /// Cached line count from last rebuild cached_line_count: usize, /// Cached total wrap row count from last rebuild. /// Used together with `cached_line_count` to detect if the cache is stale. /// When soft wrap changes a line's wrap count without changing buffer line count, /// this catches the staleness. cached_wrap_row_count: usize, } impl WrapMap { pub fn new(font: Font, font_size: Pixels, wrap_width: Option) -> Self { Self { wrapper: TextWrapper::new(font, font_size, wrap_width), buffer_line_starts: Vec::new(), cached_line_count: 0, cached_wrap_row_count: 0, } } /// Get total number of wrap rows (visual rows after soft-wrapping) #[inline] pub fn wrap_row_count(&self) -> usize { self.wrapper.len() } /// Get total number of buffer lines (logical lines) #[inline] pub fn buffer_line_count(&self) -> usize { self.wrapper.lines.len() } /// Get the buffer line for a given wrap row pub fn wrap_row_to_buffer_line(&self, wrap_row: usize) -> usize { if wrap_row >= self.wrap_row_count() { return self.buffer_line_count().saturating_sub(1); } // Binary search in prefix sum cache match self.buffer_line_starts.binary_search(&wrap_row) { Ok(line) => line, Err(insert_pos) => insert_pos.saturating_sub(1), } } /// Get the first wrap row for a given buffer line pub fn buffer_line_to_first_wrap_row(&self, line: usize) -> usize { if line >= self.buffer_line_starts.len() { return self.wrap_row_count(); } self.buffer_line_starts[line] } /// Get the wrap row range for a buffer line: [start, end) pub fn buffer_line_to_wrap_row_range(&self, line: usize) -> Range { let start = self.buffer_line_to_first_wrap_row(line); let end = if line + 1 < self.buffer_line_starts.len() { self.buffer_line_starts[line + 1] } else { self.wrap_row_count() }; start..end } /// Update text (incremental or full) pub fn on_text_changed( &mut self, changed_text: &Rope, range: &Range, new_text: &Rope, cx: &mut App, ) { self.wrapper.update(changed_text, range, new_text, cx); self.rebuild_cache(); } /// Update layout parameters (wrap width or font) pub fn on_layout_changed(&mut self, wrap_width: Option, cx: &mut App) { self.wrapper.set_wrap_width(wrap_width, cx); self.rebuild_cache(); } /// Set font parameters pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { self.wrapper.set_font(font, font_size, cx); self.rebuild_cache(); } /// Ensure text is prepared (initializes wrapper if needed) pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) -> bool { let did_initialize = self.wrapper.prepare_if_need(text, cx); if did_initialize { self.rebuild_cache(); } did_initialize } /// Initialize with text pub fn set_text(&mut self, text: &Rope, cx: &mut App) { self.wrapper.set_default_text(text); self.wrapper.prepare_if_need(text, cx); self.rebuild_cache(); } /// Rebuild the prefix sum cache: buffer_line_starts fn rebuild_cache(&mut self) { let line_count = self.wrapper.lines.len(); let wrap_row_count = self.wrapper.len(); // Skip if nothing changed: both buffer line count and total wrap row count must match. if line_count == self.cached_line_count && wrap_row_count == self.cached_wrap_row_count && !self.buffer_line_starts.is_empty() { return; } self.buffer_line_starts.clear(); let mut wrap_row = 0; for line_item in &self.wrapper.lines { self.buffer_line_starts.push(wrap_row); wrap_row += line_item.lines_len(); } self.cached_line_count = line_count; self.cached_wrap_row_count = wrap_row_count; } /// Get access to the underlying wrapper (for rendering/hit-testing) pub(crate) fn wrapper(&self) -> &TextWrapper { &self.wrapper } /// Get access to line items (for rendering) pub(crate) fn lines(&self) -> &[LineItem] { &self.wrapper.lines } /// Get the rope text pub fn text(&self) -> &Rope { self.wrapper.text() } /// Calculate how many wrap rows of a buffer line are visible. /// Without folding, all wrap rows are visible. pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize { self.buffer_line_to_wrap_row_range(line).len() } }