173 lines
5.5 KiB
Rust
173 lines
5.5 KiB
Rust
/// 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<usize>,
|
|
|
|
/// 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<Pixels>) -> 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<usize> {
|
|
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<usize>,
|
|
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<Pixels>, 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()
|
|
}
|
|
}
|