fix: chat input crashing when moving the cursor (#33)

Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
2026-06-03 13:18:03 +00:00
parent 5d4c8634ef
commit c78e0a5163
42 changed files with 7175 additions and 1593 deletions

View File

@@ -0,0 +1,222 @@
/// 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::fold_map::FoldMap;
use super::text_wrapper::{LineItem, TextWrapper, WrapDisplayPoint};
use super::{BufferPoint, WrapPoint};
use crate::input::rope_ext::RopeExt;
/// 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()
}
/// Convert buffer position to wrap position
pub(super) fn buffer_pos_to_wrap_pos(&self, pos: BufferPoint) -> WrapPoint {
let BufferPoint { line, col } = pos;
// Clamp to valid range
let line = line.min(self.buffer_line_count().saturating_sub(1));
let line_item = self.wrapper.lines.get(line);
let col = if let Some(line_item) = line_item {
col.min(line_item.len())
} else {
0
};
// Calculate offset in rope
let line_start_offset = self.wrapper.text().line_start_offset(line);
let offset = line_start_offset + col;
// Use TextWrapper's existing conversion
let display_point = self.wrapper.offset_to_display_point(offset);
WrapPoint::new(display_point.row, display_point.column)
}
/// Convert wrap position to buffer position
pub(super) fn wrap_pos_to_buffer_pos(&self, pos: WrapPoint) -> BufferPoint {
let WrapPoint { row, col } = pos;
// Clamp wrap_row to valid range
let row = row.min(self.wrap_row_count().saturating_sub(1));
// Use TextWrapper's existing conversion
let display_point = WrapDisplayPoint::new(row, 0, col);
let offset = self.wrapper.display_point_to_offset(display_point);
// Convert offset to buffer position
let point = self.wrapper.text().offset_to_point(offset);
let line_start = self.wrapper.text().line_start_offset(point.row);
let col = offset.saturating_sub(line_start);
BufferPoint::new(point.row, col)
}
/// 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.
// Checking wrap_row_count is essential because soft-wrap can change the number of
// wrap rows per line without changing the buffer line count.
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 (not folded)
pub fn visible_wrap_row_count_for_line(&self, line: usize, fold_map: &FoldMap) -> usize {
let wrap_range = self.buffer_line_to_wrap_row_range(line);
wrap_range
.filter(|&wr| fold_map.wrap_row_to_display_row(wr).is_some())
.count()
}
}