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:
336
crates/ui/src/input/display_map/display_map.rs
Normal file
336
crates/ui/src/input/display_map/display_map.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
/// DisplayMap: Public facade for Editor/Input display mapping.
|
||||
///
|
||||
/// This combines WrapMap and FoldMap to provide a unified API:
|
||||
/// - BufferPoint ↔ DisplayPoint conversion
|
||||
/// - Fold management (candidates, toggle, query)
|
||||
/// - Automatic projection updates on text/layout changes
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{App, Font, Pixels};
|
||||
use ropey::Rope;
|
||||
|
||||
use super::fold_map::FoldMap;
|
||||
use super::folding::FoldRange;
|
||||
use super::text_wrapper::{LineItem, WrapDisplayPoint};
|
||||
use super::wrap_map::WrapMap;
|
||||
use super::{BufferPoint, DisplayPoint};
|
||||
use crate::input::display_map::WrapPoint;
|
||||
use crate::input::rope_ext::RopeExt as _;
|
||||
use crate::input::Point as TreeSitterPoint;
|
||||
|
||||
/// DisplayMap is the main interface for Editor/Input coordinate mapping.
|
||||
///
|
||||
/// It manages the two-layer projection:
|
||||
/// 1. Buffer → Wrap (soft-wrapping)
|
||||
/// 2. Wrap → Display (folding)
|
||||
///
|
||||
/// Editor/Input only needs to work with BufferPoint and DisplayPoint.
|
||||
pub struct DisplayMap {
|
||||
wrap_map: WrapMap,
|
||||
fold_map: FoldMap,
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
pub fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||
Self {
|
||||
wrap_map: WrapMap::new(font, font_size, wrap_width),
|
||||
fold_map: FoldMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Core Coordinate Mapping ====================
|
||||
|
||||
/// Convert buffer position to display position
|
||||
pub fn buffer_pos_to_display_pos(&self, pos: BufferPoint) -> DisplayPoint {
|
||||
// Buffer → Wrap
|
||||
let wrap_pos = self.wrap_map.buffer_pos_to_wrap_pos(pos);
|
||||
|
||||
// Wrap → Display
|
||||
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_pos.row) {
|
||||
DisplayPoint::new(display_row, wrap_pos.col)
|
||||
} else {
|
||||
// Cursor is in a folded region, find nearest visible row
|
||||
let display_row = self.fold_map.nearest_visible_display_row(wrap_pos.row);
|
||||
DisplayPoint::new(display_row, 0) // Column 0 at fold boundary
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert display position to buffer position
|
||||
pub fn display_pos_to_buffer_pos(&self, pos: DisplayPoint) -> BufferPoint {
|
||||
// Display → Wrap
|
||||
let wrap_row = self.fold_map.display_row_to_wrap_row(pos.row).unwrap_or(0);
|
||||
|
||||
// Wrap → Buffer
|
||||
let wrap_pos = WrapPoint::new(wrap_row, pos.col);
|
||||
self.wrap_map.wrap_pos_to_buffer_pos(wrap_pos)
|
||||
}
|
||||
|
||||
/// Get total number of visible display rows
|
||||
#[inline]
|
||||
pub fn display_row_count(&self) -> usize {
|
||||
self.fold_map.display_row_count()
|
||||
}
|
||||
|
||||
/// Get the buffer line for a given display row
|
||||
pub fn display_row_to_buffer_line(&self, display_row: usize) -> usize {
|
||||
// Display → Wrap
|
||||
let wrap_row = self
|
||||
.fold_map
|
||||
.display_row_to_wrap_row(display_row)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Wrap → Buffer line
|
||||
self.wrap_map.wrap_row_to_buffer_line(wrap_row)
|
||||
}
|
||||
|
||||
/// Get the display row range for a buffer line: [start, end)
|
||||
/// Returns None if the buffer line is completely hidden
|
||||
pub fn buffer_line_to_display_row_range(&self, line: usize) -> Option<Range<usize>> {
|
||||
// Buffer line → Wrap row range
|
||||
let wrap_row_range = self.wrap_map.buffer_line_to_wrap_row_range(line);
|
||||
|
||||
// Find first and last visible display rows in this range
|
||||
let mut first_display_row = None;
|
||||
let mut last_display_row = None;
|
||||
|
||||
for wrap_row in wrap_row_range {
|
||||
if let Some(display_row) = self.fold_map.wrap_row_to_display_row(wrap_row) {
|
||||
if first_display_row.is_none() {
|
||||
first_display_row = Some(display_row);
|
||||
}
|
||||
last_display_row = Some(display_row);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(start), Some(end)) = (first_display_row, last_display_row) {
|
||||
Some(start..end + 1)
|
||||
} else {
|
||||
None // Completely folded
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a buffer line is completely hidden
|
||||
#[inline]
|
||||
pub fn is_buffer_line_hidden(&self, line: usize) -> bool {
|
||||
self.buffer_line_to_display_row_range(line).is_none()
|
||||
}
|
||||
|
||||
/// Set fold candidates (from tree-sitter/LSP)
|
||||
pub fn set_fold_candidates(&mut self, candidates: Vec<FoldRange>) {
|
||||
self.fold_map.set_candidates(candidates);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Set a fold at the given start_line (must be in candidates)
|
||||
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
||||
self.fold_map.set_folded(start_line, folded);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Toggle fold at the given start_line
|
||||
pub fn toggle_fold(&mut self, start_line: usize) {
|
||||
self.fold_map.toggle_fold(start_line);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Check if a line is currently folded
|
||||
#[inline]
|
||||
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
||||
self.fold_map.is_folded_at(start_line)
|
||||
}
|
||||
|
||||
/// Check if a line is a fold candidate
|
||||
#[inline]
|
||||
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
||||
self.fold_map.is_fold_candidate(start_line)
|
||||
}
|
||||
|
||||
/// Get all currently folded ranges
|
||||
#[inline]
|
||||
pub fn folded_ranges(&self) -> &[FoldRange] {
|
||||
self.fold_map.folded_ranges()
|
||||
}
|
||||
|
||||
/// Clear all folds
|
||||
pub fn clear_folds(&mut self) {
|
||||
self.fold_map.clear_folds();
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
// ==================== Text and Layout Updates ====================
|
||||
|
||||
/// Adjust folds and candidates for a text edit before updating the wrap map.
|
||||
///
|
||||
/// Must be called with the OLD text (before replacement) and the edit range/new_text
|
||||
/// so we can compute which old lines were affected.
|
||||
pub fn adjust_folds_for_edit(&mut self, old_text: &Rope, range: &Range<usize>, new_text: &str) {
|
||||
if self.fold_map.folded_ranges().is_empty() && self.fold_map.fold_candidates().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let edit_start_line = old_text.offset_to_point(range.start).row;
|
||||
let edit_end_line = old_text.offset_to_point(range.end.min(old_text.len())).row;
|
||||
|
||||
let old_lines_in_range = edit_end_line.saturating_sub(edit_start_line);
|
||||
let new_lines_in_range = new_text.chars().filter(|c| *c == '\n').count();
|
||||
let line_delta = new_lines_in_range as isize - old_lines_in_range as isize;
|
||||
|
||||
self.fold_map
|
||||
.adjust_folds_for_edit(edit_start_line, edit_end_line, line_delta);
|
||||
}
|
||||
|
||||
/// Incrementally update fold candidates after a text edit.
|
||||
///
|
||||
/// Extracts new fold candidates only within the edited byte range
|
||||
/// and merges them with existing (already adjusted) candidates.
|
||||
pub fn update_fold_candidates_for_edit(
|
||||
&mut self,
|
||||
tree: &super::folding::Tree,
|
||||
edit_byte_range: Range<usize>,
|
||||
new_text: &Rope,
|
||||
) {
|
||||
let new_start_line = new_text.offset_to_point(edit_byte_range.start).row;
|
||||
let new_end_line = new_text
|
||||
.offset_to_point(edit_byte_range.end.min(new_text.len()))
|
||||
.row;
|
||||
|
||||
let new_candidates = super::folding::extract_fold_ranges_in_range(tree, edit_byte_range);
|
||||
self.fold_map
|
||||
.merge_candidates_for_edit(new_start_line, new_end_line, new_candidates);
|
||||
}
|
||||
|
||||
/// 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.wrap_map
|
||||
.on_text_changed(changed_text, range, new_text, cx);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Update layout parameters (wrap width or font)
|
||||
pub fn on_layout_changed(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
self.wrap_map.on_layout_changed(wrap_width, cx);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Set font parameters
|
||||
pub fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||
self.wrap_map.set_font(font, font_size, cx);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
/// Ensure text is prepared (initializes wrapper if needed)
|
||||
pub fn ensure_text_prepared(&mut self, text: &Rope, cx: &mut App) {
|
||||
let did_initialize = self.wrap_map.ensure_text_prepared(text, cx);
|
||||
if did_initialize {
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize with text
|
||||
pub fn set_text(&mut self, text: &Rope, cx: &mut App) {
|
||||
self.wrap_map.set_text(text, cx);
|
||||
self.rebuild_fold_projection();
|
||||
}
|
||||
|
||||
// ==================== Internal Helpers ====================
|
||||
|
||||
/// Rebuild fold projection after wrap_map or fold state changes
|
||||
/// Only rebuilds if there are actually folded ranges
|
||||
fn rebuild_fold_projection(&mut self) {
|
||||
if !self.fold_map.folded_ranges().is_empty() {
|
||||
self.fold_map.rebuild(&self.wrap_map);
|
||||
} else {
|
||||
// No active folds: identity mapping (wrap_row == display_row).
|
||||
// Just update cached count so query methods work without Vec allocation.
|
||||
self.fold_map
|
||||
.mark_dirty_with_wrap_count(self.wrap_map.wrap_row_count());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Wrap Display Point Operations ====================
|
||||
|
||||
/// Convert byte offset to wrap display point (with soft wrap info).
|
||||
#[inline]
|
||||
pub(crate) fn offset_to_wrap_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||
self.wrap_map.wrapper().offset_to_display_point(offset)
|
||||
}
|
||||
|
||||
/// Convert wrap display point to byte offset.
|
||||
#[inline]
|
||||
pub(crate) fn wrap_display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||
self.wrap_map.wrapper().display_point_to_offset(point)
|
||||
}
|
||||
|
||||
/// Convert wrap display point to TreeSitterPoint (buffer line/col).
|
||||
#[inline]
|
||||
pub(crate) fn wrap_display_point_to_point(
|
||||
&self,
|
||||
point: WrapDisplayPoint,
|
||||
) -> TreeSitterPoint {
|
||||
self.wrap_map.wrapper().display_point_to_point(point)
|
||||
}
|
||||
|
||||
/// Convert a wrap row to a display row (skipping folded rows).
|
||||
/// Returns None if the wrap row is folded.
|
||||
#[inline]
|
||||
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||
self.fold_map.wrap_row_to_display_row(wrap_row)
|
||||
}
|
||||
|
||||
/// Find the nearest visible display row for a given wrap row.
|
||||
#[inline]
|
||||
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||
self.fold_map.nearest_visible_display_row(wrap_row)
|
||||
}
|
||||
|
||||
/// Convert a display row to a wrap row.
|
||||
#[inline]
|
||||
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||
self.fold_map.display_row_to_wrap_row(display_row)
|
||||
}
|
||||
|
||||
/// Get the longest row index (by byte length).
|
||||
#[inline]
|
||||
pub(crate) fn longest_row(&self) -> usize {
|
||||
self.wrap_map.wrapper().longest_row.row
|
||||
}
|
||||
|
||||
// ==================== Access Methods ====================
|
||||
|
||||
/// Get access to line items (for rendering)
|
||||
#[inline]
|
||||
pub(crate) fn lines(&self) -> &[LineItem] {
|
||||
self.wrap_map.lines()
|
||||
}
|
||||
|
||||
/// Get the rope text
|
||||
#[inline]
|
||||
pub fn text(&self) -> &Rope {
|
||||
self.wrap_map.text()
|
||||
}
|
||||
|
||||
/// Calculate how many wrap rows of a buffer line are visible (not folded)
|
||||
#[inline]
|
||||
pub fn visible_wrap_row_count_for_buffer_line(&self, line: usize) -> usize {
|
||||
self.wrap_map
|
||||
.visible_wrap_row_count_for_line(line, &self.fold_map)
|
||||
}
|
||||
|
||||
/// Get the wrap row count (before folding)
|
||||
#[inline]
|
||||
pub fn wrap_row_count(&self) -> usize {
|
||||
self.wrap_map.wrap_row_count()
|
||||
}
|
||||
|
||||
/// Get the buffer line count (logical lines)
|
||||
#[inline]
|
||||
pub fn buffer_line_count(&self) -> usize {
|
||||
self.wrap_map.buffer_line_count()
|
||||
}
|
||||
}
|
||||
343
crates/ui/src/input/display_map/fold_map.rs
Normal file
343
crates/ui/src/input/display_map/fold_map.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
/// FoldMap: Folding projection layer (Wrap rows → Display rows).
|
||||
///
|
||||
/// This module manages code folding by:
|
||||
/// - Filtering out wrap rows that belong to folded regions
|
||||
/// - Maintaining bidirectional mapping: wrap_row ↔ display_row
|
||||
/// - Handling fold state changes and rebuilding the projection
|
||||
use super::folding::FoldRange;
|
||||
use super::wrap_map::WrapMap;
|
||||
|
||||
/// FoldMap projects wrap rows to display rows by hiding folded regions.
|
||||
pub struct FoldMap {
|
||||
/// Mapping: display_row → wrap_row
|
||||
/// index = display_row, value = actual wrap_row
|
||||
visible_wrap_rows: Vec<usize>,
|
||||
|
||||
/// Reverse mapping: wrap_row → display_row
|
||||
/// index = wrap_row, value = Some(display_row) if visible, None if folded
|
||||
wrap_row_to_display_row: Vec<Option<usize>>,
|
||||
|
||||
/// Candidate fold ranges (from tree-sitter/LSP)
|
||||
/// Sorted by start_line, unique start_line
|
||||
candidates: Vec<FoldRange>,
|
||||
|
||||
/// Currently folded ranges
|
||||
/// Subset of candidates, sorted by start_line
|
||||
folded: Vec<FoldRange>,
|
||||
|
||||
/// Flag indicating if the fold projection needs rebuilding
|
||||
/// Used for lazy evaluation to avoid expensive rebuilds on every text change
|
||||
needs_rebuild: bool,
|
||||
|
||||
/// Cached wrap_row_count from last rebuild
|
||||
/// Used to detect if WrapMap changed and rebuild is needed
|
||||
cached_wrap_row_count: usize,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
visible_wrap_rows: Vec::new(),
|
||||
wrap_row_to_display_row: Vec::new(),
|
||||
candidates: Vec::new(),
|
||||
folded: Vec::new(),
|
||||
needs_rebuild: true,
|
||||
cached_wrap_row_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update cached wrap_row_count without full rebuild.
|
||||
/// Used when no folds are active (identity mapping assumed).
|
||||
pub(super) fn mark_dirty_with_wrap_count(&mut self, wrap_row_count: usize) {
|
||||
self.needs_rebuild = true;
|
||||
self.cached_wrap_row_count = wrap_row_count;
|
||||
}
|
||||
|
||||
/// Get total number of visible display rows
|
||||
pub fn display_row_count(&self) -> usize {
|
||||
if self.folded.is_empty() {
|
||||
return self.cached_wrap_row_count;
|
||||
}
|
||||
self.visible_wrap_rows.len()
|
||||
}
|
||||
|
||||
/// Convert wrap_row to display_row
|
||||
/// Returns None if the wrap_row is hidden by folding
|
||||
pub fn wrap_row_to_display_row(&self, wrap_row: usize) -> Option<usize> {
|
||||
if self.folded.is_empty() {
|
||||
return if wrap_row < self.cached_wrap_row_count {
|
||||
Some(wrap_row)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
self.wrap_row_to_display_row
|
||||
.get(wrap_row)
|
||||
.copied()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Convert display_row to wrap_row
|
||||
pub fn display_row_to_wrap_row(&self, display_row: usize) -> Option<usize> {
|
||||
if self.folded.is_empty() {
|
||||
return if display_row < self.cached_wrap_row_count {
|
||||
Some(display_row)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
self.visible_wrap_rows.get(display_row).copied()
|
||||
}
|
||||
|
||||
/// Find the nearest visible display_row for a given wrap_row
|
||||
pub fn nearest_visible_display_row(&self, wrap_row: usize) -> usize {
|
||||
if self.folded.is_empty() {
|
||||
return wrap_row.min(self.cached_wrap_row_count.saturating_sub(1));
|
||||
}
|
||||
|
||||
if let Some(dr) = self.wrap_row_to_display_row(wrap_row) {
|
||||
return dr;
|
||||
}
|
||||
|
||||
match self.visible_wrap_rows.binary_search(&wrap_row) {
|
||||
Ok(idx) => idx,
|
||||
Err(insert_pos) => insert_pos.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set fold candidates (from tree-sitter/LSP), full replacement.
|
||||
pub fn set_candidates(&mut self, mut candidates: Vec<FoldRange>) {
|
||||
// Sort and deduplicate by start_line
|
||||
candidates.sort_by_key(|r| r.start_line);
|
||||
candidates.dedup_by_key(|r| r.start_line);
|
||||
self.candidates = candidates;
|
||||
|
||||
// Remove any folded ranges that are no longer in candidates
|
||||
self.folded.retain(|fold| {
|
||||
self.candidates
|
||||
.iter()
|
||||
.any(|c| c.start_line == fold.start_line)
|
||||
});
|
||||
}
|
||||
|
||||
/// Merge new candidates extracted from an edited region into existing candidates.
|
||||
///
|
||||
/// Replaces candidates within [edit_start_line, edit_end_line] with `new_candidates`,
|
||||
/// keeping candidates outside the edit range intact.
|
||||
pub fn merge_candidates_for_edit(
|
||||
&mut self,
|
||||
edit_start_line: usize,
|
||||
edit_end_line: usize,
|
||||
new_candidates: Vec<FoldRange>,
|
||||
) {
|
||||
// Remove old candidates within the edit range (already done by adjust_folds_for_edit)
|
||||
// But do it again in case adjust wasn't called or range differs
|
||||
self.candidates
|
||||
.retain(|c| c.start_line < edit_start_line || c.start_line > edit_end_line);
|
||||
|
||||
// Add new candidates
|
||||
self.candidates.extend(new_candidates);
|
||||
self.candidates.sort_by_key(|r| r.start_line);
|
||||
self.candidates.dedup_by_key(|r| r.start_line);
|
||||
}
|
||||
|
||||
/// Set a fold at the given start_line (must be in candidates)
|
||||
pub fn set_folded(&mut self, start_line: usize, folded: bool) {
|
||||
if folded {
|
||||
// Find the candidate range for this start_line
|
||||
if let Some(candidate) = self.candidates.iter().find(|c| c.start_line == start_line) {
|
||||
// Add to folded if not already present
|
||||
if !self.folded.iter().any(|f| f.start_line == start_line) {
|
||||
self.folded.push(*candidate);
|
||||
self.folded.sort_by_key(|r| r.start_line);
|
||||
self.needs_rebuild = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove from folded
|
||||
self.folded.retain(|f| f.start_line != start_line);
|
||||
self.needs_rebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fold at the given start_line
|
||||
pub fn toggle_fold(&mut self, start_line: usize) {
|
||||
let is_folded = self.is_folded_at(start_line);
|
||||
self.set_folded(start_line, !is_folded);
|
||||
}
|
||||
|
||||
/// Check if a line is currently folded
|
||||
pub fn is_folded_at(&self, start_line: usize) -> bool {
|
||||
self.folded.iter().any(|f| f.start_line == start_line)
|
||||
}
|
||||
|
||||
/// Check if a line is a fold candidate
|
||||
pub fn is_fold_candidate(&self, start_line: usize) -> bool {
|
||||
self.candidates.iter().any(|c| c.start_line == start_line)
|
||||
}
|
||||
|
||||
/// Get all fold candidates
|
||||
#[inline]
|
||||
pub fn fold_candidates(&self) -> &[FoldRange] {
|
||||
&self.candidates
|
||||
}
|
||||
|
||||
/// Get all currently folded ranges
|
||||
#[inline]
|
||||
pub fn folded_ranges(&self) -> &[FoldRange] {
|
||||
&self.folded
|
||||
}
|
||||
|
||||
/// Clear all folds
|
||||
#[inline]
|
||||
pub fn clear_folds(&mut self) {
|
||||
self.folded.clear();
|
||||
}
|
||||
|
||||
/// Adjust folds and candidates after a text edit.
|
||||
///
|
||||
/// - Folds/candidates overlapping the edited line range are removed
|
||||
/// - Folds/candidates after the edit are shifted by line_delta
|
||||
///
|
||||
/// This avoids expensive full tree traversal on every keystroke.
|
||||
pub fn adjust_folds_for_edit(
|
||||
&mut self,
|
||||
edit_start_line: usize,
|
||||
edit_end_line: usize,
|
||||
line_delta: isize,
|
||||
) {
|
||||
// Adjust folded ranges
|
||||
if !self.folded.is_empty() {
|
||||
self.folded.retain(|fold| {
|
||||
!(fold.start_line <= edit_end_line && fold.end_line >= edit_start_line)
|
||||
});
|
||||
|
||||
if line_delta != 0 {
|
||||
for fold in &mut self.folded {
|
||||
if fold.start_line > edit_end_line {
|
||||
fold.start_line = (fold.start_line as isize + line_delta).max(0) as usize;
|
||||
fold.end_line = (fold.end_line as isize + line_delta).max(0) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust candidates the same way
|
||||
if !self.candidates.is_empty() {
|
||||
self.candidates
|
||||
.retain(|c| !(c.start_line <= edit_end_line && c.end_line >= edit_start_line));
|
||||
|
||||
if line_delta != 0 {
|
||||
for c in &mut self.candidates {
|
||||
if c.start_line > edit_end_line {
|
||||
c.start_line = (c.start_line as isize + line_delta).max(0) as usize;
|
||||
c.end_line = (c.end_line as isize + line_delta).max(0) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.needs_rebuild = true;
|
||||
}
|
||||
|
||||
/// Rebuild the fold mapping after wrap_map or fold state changes
|
||||
///
|
||||
/// This is the core algorithm that projects wrap rows to display rows.
|
||||
pub fn rebuild(&mut self, wrap_map: &WrapMap) {
|
||||
let wrap_row_count = wrap_map.wrap_row_count();
|
||||
|
||||
// Performance optimization: skip rebuild if nothing changed
|
||||
if !self.needs_rebuild && wrap_row_count == self.cached_wrap_row_count {
|
||||
return;
|
||||
}
|
||||
|
||||
self.cached_wrap_row_count = wrap_row_count;
|
||||
|
||||
self.visible_wrap_rows.clear();
|
||||
self.wrap_row_to_display_row = vec![None; wrap_row_count];
|
||||
|
||||
if self.folded.is_empty() {
|
||||
// Fast path: no folds, all wrap rows are visible
|
||||
self.visible_wrap_rows = (0..wrap_row_count).collect();
|
||||
for (display_row, &wrap_row) in self.visible_wrap_rows.iter().enumerate() {
|
||||
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
||||
}
|
||||
self.needs_rebuild = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build set of hidden wrap_row ranges from folded buffer lines
|
||||
let mut hidden_ranges = Vec::new();
|
||||
for fold in &self.folded {
|
||||
// Hide wrap rows from (start_line + 1) to (end_line - 1) (inclusive)
|
||||
// Both the first line and last line of the fold remain visible
|
||||
let hide_start_line = fold.start_line + 1;
|
||||
let hide_end_line = fold.end_line.saturating_sub(1);
|
||||
|
||||
if hide_start_line > hide_end_line {
|
||||
continue; // No middle lines to hide (0 or 1 lines between start and end)
|
||||
}
|
||||
|
||||
// Get wrap_row ranges for the hidden buffer lines
|
||||
let start_wrap_row = wrap_map.buffer_line_to_first_wrap_row(hide_start_line);
|
||||
let end_wrap_row = if hide_end_line + 1 < wrap_map.buffer_line_count() {
|
||||
wrap_map.buffer_line_to_first_wrap_row(hide_end_line + 1)
|
||||
} else {
|
||||
wrap_row_count
|
||||
};
|
||||
|
||||
if start_wrap_row < end_wrap_row {
|
||||
hidden_ranges.push(start_wrap_row..end_wrap_row);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge overlapping hidden ranges
|
||||
hidden_ranges.sort_by_key(|r| r.start);
|
||||
let mut merged_hidden = Vec::new();
|
||||
for range in hidden_ranges {
|
||||
if let Some(last) = merged_hidden.last_mut() {
|
||||
if range.start <= *last {
|
||||
// Overlapping or adjacent, merge
|
||||
*last = (*last).max(range.end);
|
||||
} else {
|
||||
merged_hidden.push(range.start);
|
||||
merged_hidden.push(range.end);
|
||||
}
|
||||
} else {
|
||||
merged_hidden.push(range.start);
|
||||
merged_hidden.push(range.end);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all wrap rows and filter out hidden ones
|
||||
let mut display_row = 0;
|
||||
let mut hidden_iter = merged_hidden.chunks_exact(2);
|
||||
let mut current_hidden = hidden_iter.next();
|
||||
|
||||
for wrap_row in 0..wrap_row_count {
|
||||
// Check if wrap_row is in current hidden range
|
||||
let is_hidden = if let Some(&[start, end]) = current_hidden {
|
||||
if wrap_row >= end {
|
||||
current_hidden = hidden_iter.next();
|
||||
if let Some(&[new_start, new_end]) = current_hidden {
|
||||
wrap_row >= new_start && wrap_row < new_end
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
wrap_row >= start && wrap_row < end
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !is_hidden {
|
||||
self.visible_wrap_rows.push(wrap_row);
|
||||
self.wrap_row_to_display_row[wrap_row] = Some(display_row);
|
||||
display_row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.needs_rebuild = false;
|
||||
}
|
||||
}
|
||||
96
crates/ui/src/input/display_map/folding.rs
Normal file
96
crates/ui/src/input/display_map/folding.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use std::ops::Range;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use tree_sitter::Node;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use tree_sitter::Tree;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
/// Stub type for tree-sitter Tree on WASM (tree-sitter not available).
|
||||
pub struct Tree;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
/// Minimum line span for a node to be considered foldable.
|
||||
const MIN_FOLD_LINES: usize = 2;
|
||||
|
||||
/// A fold range representing a foldable code region.
|
||||
///
|
||||
/// The fold range spans from start_line to end_line (inclusive).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FoldRange {
|
||||
/// Start line (inclusive)
|
||||
pub start_line: usize,
|
||||
/// End line (inclusive)
|
||||
pub end_line: usize,
|
||||
}
|
||||
|
||||
impl FoldRange {
|
||||
pub fn new(start_line: usize, end_line: usize) -> Self {
|
||||
assert!(
|
||||
start_line <= end_line,
|
||||
"fold start_line must be <= end_line"
|
||||
);
|
||||
Self {
|
||||
start_line,
|
||||
end_line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
/// Check if a named node qualifies as a fold candidate.
|
||||
///
|
||||
/// Uses a structural heuristic: any **named** node spanning ≥ MIN_FOLD_LINES
|
||||
/// is foldable. tree-sitter already parses code into semantic units (functions,
|
||||
/// classes, blocks, etc.), so named nodes naturally correspond to meaningful
|
||||
/// foldable regions across all languages without a per-language node-type list.
|
||||
fn is_foldable_node(node: &Node) -> bool {
|
||||
let start = node.start_position().row;
|
||||
let end = node.end_position().row;
|
||||
end.saturating_sub(start) >= MIN_FOLD_LINES
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
/// Extract fold ranges only within a byte range (for incremental updates after edits).
|
||||
///
|
||||
/// Skips subtrees entirely outside the range, making it O(nodes in range)
|
||||
/// instead of O(all nodes in tree).
|
||||
pub fn extract_fold_ranges_in_range(tree: &Tree, byte_range: Range<usize>) -> Vec<FoldRange> {
|
||||
let mut ranges = Vec::new();
|
||||
let root = tree.root_node();
|
||||
let mut cursor = root.walk();
|
||||
// Skip the root, it's not foldable. Use named_children to skip literal tokens.
|
||||
for child in root.named_children(&mut cursor) {
|
||||
collect_foldable_nodes_in_range(child, &byte_range, &mut ranges);
|
||||
}
|
||||
|
||||
ranges.sort_by_key(|r| r.start_line);
|
||||
ranges.dedup_by_key(|r| r.start_line);
|
||||
ranges
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
/// Recursively collect foldable nodes, skipping subtrees outside byte_range.
|
||||
fn collect_foldable_nodes_in_range(
|
||||
node: Node,
|
||||
byte_range: &Range<usize>,
|
||||
ranges: &mut Vec<FoldRange>,
|
||||
) {
|
||||
if node.end_byte() <= byte_range.start || node.start_byte() >= byte_range.end {
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_foldable_node(&node) {
|
||||
return;
|
||||
}
|
||||
|
||||
ranges.push(FoldRange {
|
||||
start_line: node.start_position().row,
|
||||
end_line: node.end_position().row,
|
||||
});
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.named_children(&mut cursor) {
|
||||
collect_foldable_nodes_in_range(child, byte_range, ranges);
|
||||
}
|
||||
}
|
||||
69
crates/ui/src/input/display_map/mod.rs
Normal file
69
crates/ui/src/input/display_map/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Display mapping system for Editor/Input.
|
||||
///
|
||||
/// This module implements a layered display mapping architecture:
|
||||
/// - **WrapMap**: Handles soft-wrapping (buffer → wrap rows)
|
||||
/// - **FoldMap**: Handles folding (wrap rows → display rows)
|
||||
/// - **DisplayMap**: Public facade for Editor/Input
|
||||
///
|
||||
/// The goal is to provide a clean, unified API where Editor only needs to know
|
||||
/// about `BufferPoint ↔ DisplayPoint` mapping, without worrying about internal wrap/fold complexity.
|
||||
mod display_map;
|
||||
mod fold_map;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod folding;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub mod folding;
|
||||
mod text_wrapper;
|
||||
mod wrap_map;
|
||||
|
||||
// Re-export public API
|
||||
// Re-export FoldRange and extract_fold_ranges
|
||||
pub use folding::FoldRange;
|
||||
|
||||
pub use self::display_map::DisplayMap;
|
||||
pub(crate) use self::text_wrapper::LineLayout;
|
||||
|
||||
/// Position in the buffer (logical text).
|
||||
///
|
||||
/// - `line`: 0-based logical line number (split by `\n`)
|
||||
/// - `col`: 0-based column offset (byte offset)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct BufferPoint {
|
||||
pub line: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
impl BufferPoint {
|
||||
pub fn new(line: usize, col: usize) -> Self {
|
||||
Self { line, col }
|
||||
}
|
||||
}
|
||||
|
||||
/// Position after soft-wrapping but before folding (internal).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(super) struct WrapPoint {
|
||||
pub row: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
impl WrapPoint {
|
||||
pub fn new(row: usize, col: usize) -> Self {
|
||||
Self { row, col }
|
||||
}
|
||||
}
|
||||
|
||||
/// Final display position (after soft-wrapping and folding).
|
||||
///
|
||||
/// - `row`: 0-based display row (final visible row)
|
||||
/// - `col`: 0-based display column
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DisplayPoint {
|
||||
pub row: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
impl DisplayPoint {
|
||||
pub fn new(row: usize, col: usize) -> Self {
|
||||
Self { row, col }
|
||||
}
|
||||
}
|
||||
930
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
930
crates/ui/src/input/display_map/text_wrapper.rs
Normal file
@@ -0,0 +1,930 @@
|
||||
use std::ops::Range;
|
||||
use gpui::Half;
|
||||
|
||||
use gpui::{
|
||||
App, Font, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px,
|
||||
size,
|
||||
};
|
||||
use ropey::Rope;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::input::{LastLayout, Point as TreeSitterPoint, RopeExt, WhitespaceIndicators};
|
||||
|
||||
/// A line with soft wrapped lines info.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LineItem {
|
||||
/// The original line text, without end `\n`.
|
||||
line: Rope,
|
||||
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
|
||||
///
|
||||
/// Not contains the line end `\n`.
|
||||
pub(crate) wrapped_lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl LineItem {
|
||||
/// Get the bytes length of this line.
|
||||
#[inline]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.line.len()
|
||||
}
|
||||
|
||||
/// Get number of soft wrapped lines of this line (include the first line).
|
||||
#[inline]
|
||||
pub(crate) fn lines_len(&self) -> usize {
|
||||
self.wrapped_lines.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct LongestRow {
|
||||
/// The 0-based row index.
|
||||
pub row: usize,
|
||||
/// The bytes length of the longest line.
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
|
||||
///
|
||||
/// After use lines to calculate the scroll size of the Editor.
|
||||
pub(crate) struct TextWrapper {
|
||||
text: Rope,
|
||||
/// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||
soft_lines: usize,
|
||||
font: Font,
|
||||
font_size: Pixels,
|
||||
/// If is none, it means the text is not wrapped
|
||||
wrap_width: Option<Pixels>,
|
||||
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
|
||||
pub(crate) longest_row: LongestRow,
|
||||
/// The lines by split \n
|
||||
pub(crate) lines: Vec<LineItem>,
|
||||
|
||||
_initialized: bool,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TextWrapper {
|
||||
pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||
Self {
|
||||
text: Rope::new(),
|
||||
font,
|
||||
font_size,
|
||||
wrap_width,
|
||||
soft_lines: 0,
|
||||
longest_row: LongestRow::default(),
|
||||
lines: Vec::new(),
|
||||
_initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn set_default_text(&mut self, text: &Rope) {
|
||||
self.text = text.clone();
|
||||
}
|
||||
|
||||
/// Get reference to the rope text.
|
||||
#[inline]
|
||||
pub(crate) fn text(&self) -> &Rope {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// Get the total number of lines including wrapped lines.
|
||||
#[inline]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.soft_lines
|
||||
}
|
||||
|
||||
/// Get the line item by row index.
|
||||
#[inline]
|
||||
pub(crate) fn line(&self, row: usize) -> Option<&LineItem> {
|
||||
self.lines.iter().skip(row).next()
|
||||
}
|
||||
|
||||
pub(crate) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
if wrap_width == self.wrap_width {
|
||||
return;
|
||||
}
|
||||
|
||||
self.wrap_width = wrap_width;
|
||||
self.update_all(&self.text.clone(), cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
||||
if self.font.eq(&font) && self.font_size == font_size {
|
||||
return;
|
||||
}
|
||||
|
||||
self.font = font;
|
||||
self.font_size = font_size;
|
||||
self.update_all(&self.text.clone(), cx);
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_if_need(&mut self, text: &Rope, cx: &mut App) -> bool {
|
||||
if self._initialized {
|
||||
return false;
|
||||
}
|
||||
self._initialized = true;
|
||||
self.update_all(text, cx);
|
||||
true
|
||||
}
|
||||
|
||||
/// Update the text wrapper and recalculate the wrapped lines.
|
||||
///
|
||||
/// If the `text` is the same as the current text, do nothing.
|
||||
///
|
||||
/// - `changed_text`: The text [`Rope`] that has changed.
|
||||
/// - `range`: The `selected_range` before change.
|
||||
/// - `new_text`: The inserted text.
|
||||
/// - `force`: Whether to force the update, if false, the update will be skipped if the text is the same.
|
||||
/// - `cx`: The application context.
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
changed_text: &Rope,
|
||||
range: &Range<usize>,
|
||||
new_text: &Rope,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut line_wrapper = cx
|
||||
.text_system()
|
||||
.line_wrapper(self.font.clone(), self.font_size);
|
||||
self._update(
|
||||
changed_text,
|
||||
range,
|
||||
new_text,
|
||||
&mut |line_str, wrap_width| {
|
||||
line_wrapper
|
||||
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn _update<F>(
|
||||
&mut self,
|
||||
changed_text: &Rope,
|
||||
range: &Range<usize>,
|
||||
new_text: &Rope,
|
||||
wrap_line: &mut F,
|
||||
) where
|
||||
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
|
||||
{
|
||||
// Remove the old changed lines.
|
||||
let start_row = self.text.offset_to_point(range.start).row;
|
||||
let start_row = start_row.min(self.lines.len().saturating_sub(1));
|
||||
let end_row = self.text.offset_to_point(range.end).row;
|
||||
let end_row = end_row.min(self.lines.len().saturating_sub(1));
|
||||
let rows_range = start_row..=end_row;
|
||||
|
||||
if rows_range.contains(&self.longest_row.row) {
|
||||
self.longest_row = LongestRow::default();
|
||||
}
|
||||
|
||||
let mut longest_row_ix = self.longest_row.row;
|
||||
let mut longest_row_len = self.longest_row.len;
|
||||
|
||||
// To add the new lines.
|
||||
let new_start_row = changed_text.offset_to_point(range.start).row;
|
||||
let new_start_offset = changed_text.line_start_offset(new_start_row);
|
||||
let new_end_row = changed_text
|
||||
.offset_to_point(range.start + new_text.len())
|
||||
.row;
|
||||
let new_end_offset = changed_text.line_end_offset(new_end_row);
|
||||
let new_range = new_start_offset..new_end_offset;
|
||||
|
||||
let mut new_lines = vec![];
|
||||
let wrap_width = self.wrap_width;
|
||||
|
||||
// line not contains `\n`.
|
||||
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
||||
.iter_lines()
|
||||
.enumerate()
|
||||
{
|
||||
let line_str = line.to_string();
|
||||
let mut wrapped_lines = vec![];
|
||||
let mut prev_boundary_ix = 0;
|
||||
|
||||
if line_str.len() > longest_row_len {
|
||||
longest_row_ix = new_start_row + ix;
|
||||
longest_row_len = line_str.len();
|
||||
}
|
||||
|
||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||
if let Some(wrap_width) = wrap_width {
|
||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
||||
for boundary in wrap_line(&line_str, wrap_width) {
|
||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||
prev_boundary_ix = boundary.ix;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset of the line
|
||||
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||
}
|
||||
|
||||
new_lines.push(LineItem {
|
||||
line: Rope::from(line),
|
||||
wrapped_lines,
|
||||
});
|
||||
}
|
||||
|
||||
if self.lines.len() == 0 {
|
||||
self.lines = new_lines;
|
||||
} else {
|
||||
self.lines.splice(rows_range, new_lines);
|
||||
}
|
||||
|
||||
self.text = changed_text.clone();
|
||||
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
|
||||
self.longest_row = LongestRow {
|
||||
row: longest_row_ix,
|
||||
len: longest_row_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the text wrapper and recalculate the wrapped lines.
|
||||
///
|
||||
/// If the `text` is the same as the current text, do nothing.
|
||||
fn update_all(&mut self, text: &Rope, cx: &mut App) {
|
||||
self.update(text, &(0..text.len()), &text, cx);
|
||||
}
|
||||
|
||||
/// Return display point (with soft wrap) from the given byte offset in the text.
|
||||
///
|
||||
/// Panics if the `offset` is out of bounds.
|
||||
pub(crate) fn offset_to_display_point(&self, offset: usize) -> WrapDisplayPoint {
|
||||
let row = self.text.offset_to_point(offset).row;
|
||||
let start = self.text.line_start_offset(row);
|
||||
let line = &self.lines[row];
|
||||
|
||||
let mut wrapped_row = self
|
||||
.lines
|
||||
.iter()
|
||||
.take(row)
|
||||
.map(|l| l.lines_len())
|
||||
.sum::<usize>();
|
||||
|
||||
let local_offset = offset.saturating_sub(start);
|
||||
for (ix, range) in line.wrapped_lines.iter().enumerate() {
|
||||
if range.contains(&local_offset) {
|
||||
return WrapDisplayPoint::new(
|
||||
wrapped_row + ix,
|
||||
ix,
|
||||
local_offset.saturating_sub(range.start),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise return the eof of the line.
|
||||
let last_range = line.wrapped_lines.last().unwrap_or(&(0..0));
|
||||
let ix = line.lines_len().saturating_sub(1);
|
||||
return WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len());
|
||||
}
|
||||
|
||||
/// Return byte offset in the text from the given display point (with soft wrap).
|
||||
///
|
||||
/// Panics if the `point.row` is out of bounds.
|
||||
pub(crate) fn display_point_to_offset(&self, point: WrapDisplayPoint) -> usize {
|
||||
let mut wrapped_row = 0;
|
||||
for (row, line) in self.lines.iter().enumerate() {
|
||||
if wrapped_row + line.lines_len() > point.row {
|
||||
let line_start = self.text.line_start_offset(row);
|
||||
let local_row = point.row.saturating_sub(wrapped_row);
|
||||
if let Some(range) = line.wrapped_lines.get(local_row) {
|
||||
return line_start + (range.start + point.column).min(range.end);
|
||||
} else {
|
||||
// If not found, return the end of the line.
|
||||
return line_start + line.len();
|
||||
}
|
||||
}
|
||||
|
||||
wrapped_row += line.lines_len();
|
||||
}
|
||||
|
||||
return self.text.len();
|
||||
}
|
||||
|
||||
pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint {
|
||||
let offset = self.display_point_to_offset(point);
|
||||
self.text.offset_to_point(offset)
|
||||
}
|
||||
|
||||
pub(crate) fn point_to_display_point(&self, point: TreeSitterPoint) -> WrapDisplayPoint {
|
||||
let offset = self.text.point_to_offset(point);
|
||||
self.offset_to_display_point(offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// A display point within the soft-wrapped text.
|
||||
///
|
||||
/// This represents a position in the text after soft-wrapping,
|
||||
/// with an additional `local_row` field tracking the wrap line
|
||||
/// within the original buffer line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct WrapDisplayPoint {
|
||||
/// The 0-based soft wrapped row index in the text.
|
||||
pub row: usize,
|
||||
/// The 0-based row index in local line (include first line).
|
||||
///
|
||||
/// This value only valid when return from [`TextWrapper::offset_to_display_point`], otherwise it will be ignored.
|
||||
pub local_row: usize,
|
||||
/// The 0-based column byte index in the display line (with soft wrap).
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl WrapDisplayPoint {
|
||||
pub fn new(row: usize, local_row: usize, column: usize) -> Self {
|
||||
Self {
|
||||
row,
|
||||
local_row,
|
||||
column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The layout info of a line with soft wrapped lines.
|
||||
pub(crate) struct LineLayout {
|
||||
/// Total bytes length of this line.
|
||||
len: usize,
|
||||
/// The soft wrapped lines of this line (Include the first line).
|
||||
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
||||
pub(crate) longest_width: Pixels,
|
||||
pub(crate) whitespace_indicators: Option<WhitespaceIndicators>,
|
||||
/// Whitespace indicators: (line_index, x_position, is_tab)
|
||||
pub(crate) whitespace_chars: Vec<(usize, Pixels, bool)>,
|
||||
}
|
||||
|
||||
impl LineLayout {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
len: 0,
|
||||
longest_width: px(0.),
|
||||
wrapped_lines: SmallVec::new(),
|
||||
whitespace_chars: Vec::new(),
|
||||
whitespace_indicators: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lines(mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) -> Self {
|
||||
self.set_wrapped_lines(wrapped_lines);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
||||
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
||||
let width = wrapped_lines
|
||||
.iter()
|
||||
.map(|l| l.width)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
self.longest_width = width;
|
||||
self.wrapped_lines = wrapped_lines;
|
||||
}
|
||||
|
||||
pub(crate) fn with_whitespaces(mut self, indicators: Option<WhitespaceIndicators>) -> Self {
|
||||
self.whitespace_indicators = indicators;
|
||||
let Some(indicators) = self.whitespace_indicators.as_ref() else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let space_indicator_offset = indicators.space.width.half();
|
||||
|
||||
for (line_index, wrapped_line) in self.wrapped_lines.iter().enumerate() {
|
||||
for (relative_offset, c) in wrapped_line.text.char_indices() {
|
||||
if matches!(c, ' ' | '\t') {
|
||||
let is_tab = c == '\t';
|
||||
let start_x = wrapped_line.x_for_index(relative_offset);
|
||||
let end_x = wrapped_line.x_for_index(relative_offset + c.len_utf8());
|
||||
// Center the indicator in the actual character's space
|
||||
let x_position = if c == ' ' {
|
||||
(start_x + end_x).half() - space_indicator_offset
|
||||
} else {
|
||||
start_x
|
||||
};
|
||||
|
||||
self.whitespace_chars.push((line_index, x_position, is_tab));
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
/// Get the position (x, y) for the given index in this line layout.
|
||||
///
|
||||
/// - The `offset` is a local byte index in this line layout.
|
||||
/// - When `line_end_affinity` is true, an offset at a soft wrap boundary is placed at
|
||||
/// the end of the current visual line rather than the start of the next one.
|
||||
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
||||
pub(crate) fn position_for_index(
|
||||
&self,
|
||||
offset: usize,
|
||||
last_layout: &LastLayout,
|
||||
line_end_affinity: bool,
|
||||
) -> Option<Point<Pixels>> {
|
||||
let mut acc_len = 0;
|
||||
let mut offset_y = px(0.);
|
||||
|
||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||
|
||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||
let is_last = i + 1 == self.wrapped_lines.len();
|
||||
|
||||
let matches = if line.len == 0 {
|
||||
// Empty visual lines still own their boundary offset.
|
||||
offset == acc_len
|
||||
} else if is_last || line_end_affinity {
|
||||
// Inclusive: cursor can sit at end of this visual line.
|
||||
offset >= acc_len && offset <= acc_len + line.len
|
||||
} else {
|
||||
// Exclusive: boundary offset belongs to the next visual line.
|
||||
offset >= acc_len && offset < acc_len + line.len
|
||||
};
|
||||
|
||||
if matches {
|
||||
let x = line.x_for_index(offset.saturating_sub(acc_len)) + x_offset;
|
||||
return Some(point(x, offset_y));
|
||||
}
|
||||
|
||||
// Always advance by actual line length. The last line gets +1 so the
|
||||
// cursor can be placed after the final character.
|
||||
acc_len += if is_last { line.len + 1 } else { line.len };
|
||||
offset_y += last_layout.line_height;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the closest index for the given x in this line layout.
|
||||
pub(crate) fn closest_index_for_x(&self, x: Pixels, last_layout: &LastLayout) -> usize {
|
||||
let mut acc_len = 0;
|
||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||
let x = x - x_offset;
|
||||
|
||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||
let is_last = i + 1 == self.wrapped_lines.len();
|
||||
if x <= line.width {
|
||||
let mut ix = line.closest_index_for_x(x);
|
||||
if !is_last && ix == line.text.len() {
|
||||
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||
ix = ix.saturating_sub(c_len);
|
||||
}
|
||||
|
||||
return acc_len + ix;
|
||||
}
|
||||
acc_len += line.text.len();
|
||||
}
|
||||
|
||||
acc_len
|
||||
}
|
||||
|
||||
/// Get the index for the given position (x, y) in this line layout.
|
||||
///
|
||||
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
||||
/// The return value is a local byte index in this line layout, start from 0.
|
||||
pub(crate) fn closest_index_for_position(
|
||||
&self,
|
||||
pos: Point<Pixels>,
|
||||
last_layout: &LastLayout,
|
||||
) -> Option<usize> {
|
||||
let mut offset = 0;
|
||||
let mut line_top = px(0.);
|
||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||
for (i, line) in self.wrapped_lines.iter().enumerate() {
|
||||
let is_last = i + 1 == self.wrapped_lines.len();
|
||||
let line_bottom = line_top + last_layout.line_height;
|
||||
if pos.y >= line_top && pos.y < line_bottom {
|
||||
let mut ix = line.closest_index_for_x(pos.x - x_offset);
|
||||
if !is_last && ix == line.text.len() {
|
||||
// For soft wrap line, we can't put the cursor at the end of the line.
|
||||
let c_len = line.text.chars().last().map(|c| c.len_utf8()).unwrap_or(0);
|
||||
ix = ix.saturating_sub(c_len);
|
||||
}
|
||||
return Some(offset + ix);
|
||||
}
|
||||
|
||||
offset += line.text.len();
|
||||
line_top = line_bottom;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn index_for_position(
|
||||
&self,
|
||||
pos: Point<Pixels>,
|
||||
last_layout: &LastLayout,
|
||||
) -> Option<usize> {
|
||||
let mut offset = 0;
|
||||
let mut line_top = px(0.);
|
||||
let x_offset = last_layout.alignment_offset(self.longest_width);
|
||||
for line in self.wrapped_lines.iter() {
|
||||
let line_bottom = line_top + last_layout.line_height;
|
||||
if pos.y >= line_top && pos.y < line_bottom {
|
||||
let ix = line.index_for_x(pos.x - x_offset)?;
|
||||
return Some(offset + ix);
|
||||
}
|
||||
|
||||
offset += line.text.len();
|
||||
line_top = line_bottom;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
||||
}
|
||||
|
||||
pub(crate) fn paint(
|
||||
&self,
|
||||
pos: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
text_align: TextAlign,
|
||||
align_width: Option<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
||||
_ = line.paint(
|
||||
pos + point(px(0.), ix * line_height),
|
||||
line_height,
|
||||
text_align,
|
||||
align_width,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint whitespace indicators
|
||||
if let Some(indicators) = self.whitespace_indicators.as_ref() {
|
||||
for (line_index, x_position, is_tab) in &self.whitespace_chars {
|
||||
let invisible = if *is_tab {
|
||||
indicators.tab.clone()
|
||||
} else {
|
||||
indicators.space.clone()
|
||||
};
|
||||
|
||||
let origin = point(
|
||||
pos.x + *x_position,
|
||||
pos.y + *line_index as f32 * line_height,
|
||||
);
|
||||
|
||||
_ = invisible.paint(origin, line_height, text_align, align_width, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{Boundary, FontFeatures, FontStyle, FontWeight, px};
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
let font = gpui::Font {
|
||||
family: "Arial".into(),
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::Normal,
|
||||
features: FontFeatures::default(),
|
||||
fallbacks: None,
|
||||
};
|
||||
|
||||
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
||||
let mut text = Rope::from(
|
||||
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
|
||||
);
|
||||
|
||||
fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec<Boundary> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) {
|
||||
let mut actual_lines = vec![];
|
||||
let mut offset = 0;
|
||||
for line in wrapper.lines.iter() {
|
||||
actual_lines.push(
|
||||
line.wrapped_lines
|
||||
.iter()
|
||||
.map(|range| text.slice(offset + range.start..offset + range.end))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
// +1 \n
|
||||
offset += line.len() + 1;
|
||||
}
|
||||
assert_eq!(actual_lines, expected_lines);
|
||||
}
|
||||
|
||||
wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line);
|
||||
assert_eq!(wrapper.lines.len(), 4);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["Hello, 世界!\r"],
|
||||
&["This is second line."],
|
||||
&["This is third line."],
|
||||
&["这里是第 4 行。"],
|
||||
],
|
||||
);
|
||||
|
||||
// Add a new text to end
|
||||
let range = text.len()..text.len();
|
||||
let new_text = "New text";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 4);
|
||||
assert_eq!(wrapper.lines.len(), 4);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["Hello, 世界!\r"],
|
||||
&["This is second line."],
|
||||
&["This is third line."],
|
||||
&["这里是第 4 行。New text"],
|
||||
],
|
||||
);
|
||||
|
||||
// Replace first line `Hello` to `AAA`
|
||||
let range = 0..5;
|
||||
let new_text = "AAA";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 4);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["AAA, 世界!\r"],
|
||||
&["This is second line."],
|
||||
&["This is third line."],
|
||||
&["这里是第 4 行。New text"],
|
||||
],
|
||||
);
|
||||
|
||||
// Remove the second line
|
||||
let start_offset = text.line_start_offset(1);
|
||||
let end_offset = text.line_end_offset(1);
|
||||
let range = start_offset..end_offset + 1;
|
||||
text.replace(range.clone(), "");
|
||||
wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text"
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 3);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["AAA, 世界!\r"],
|
||||
&["This is third line."],
|
||||
&["这里是第 4 行。New text"],
|
||||
],
|
||||
);
|
||||
|
||||
// Replace the first 2 lines to "This is a new line."
|
||||
let range = text.line_start_offset(0)..text.line_end_offset(1) + 1;
|
||||
let new_text = "This is a new line.\nThis is new line 2.\n";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text"
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 3);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["This is a new line."],
|
||||
&["This is new line 2."],
|
||||
&["这里是第 4 行。New text"],
|
||||
],
|
||||
);
|
||||
|
||||
// Add a new line at the end
|
||||
let range = text.len()..text.len();
|
||||
let new_text = "\nThis is a new line at the end.";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 4);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["This is a new line."],
|
||||
&["This is new line 2."],
|
||||
&["这里是第 4 行。New text"],
|
||||
&["This is a new line at the end."],
|
||||
],
|
||||
);
|
||||
|
||||
// Add a new line at the beginning
|
||||
let range = 0..0;
|
||||
let new_text = "This is a new line at the beginning.\n";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 5);
|
||||
assert_wrapper_lines(
|
||||
&text,
|
||||
&wrapper,
|
||||
&[
|
||||
&["This is a new line at the beginning."],
|
||||
&["This is a new line."],
|
||||
&["This is new line 2."],
|
||||
&["这里是第 4 行。New text"],
|
||||
&["This is a new line at the end."],
|
||||
],
|
||||
);
|
||||
|
||||
// Remove all to at least one line in `lines`.
|
||||
let range = 0..text.len();
|
||||
let new_text = "";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||
assert_eq!(text.to_string(), "");
|
||||
assert_eq!(wrapper.lines.len(), 1);
|
||||
assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]);
|
||||
|
||||
// Test update_all
|
||||
let range = 0..text.len();
|
||||
let new_text = "This is a full text.\nThis is a second line.";
|
||||
text.replace(range.clone(), new_text);
|
||||
wrapper._update(&text, &range, &text, &mut fake_wrap_line);
|
||||
assert_eq!(
|
||||
text.to_string(),
|
||||
"This is a full text.\nThis is a second line."
|
||||
);
|
||||
assert_eq!(wrapper.lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_layout() {
|
||||
let mut line_layout = LineLayout::new();
|
||||
|
||||
let line1 = ShapedLine::default().with_len(100);
|
||||
let line2 = ShapedLine::default().with_len(50);
|
||||
let wrapped_lines = smallvec::smallvec![line1, line2];
|
||||
line_layout.set_wrapped_lines(wrapped_lines);
|
||||
assert_eq!(line_layout.len(), 150);
|
||||
assert_eq!(line_layout.wrapped_lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_position_for_index_prefers_first_leading_empty_visual_line() {
|
||||
let mut line_layout = LineLayout::new();
|
||||
line_layout.set_wrapped_lines(smallvec::smallvec![
|
||||
ShapedLine::default(),
|
||||
ShapedLine::default(),
|
||||
ShapedLine::default().with_len(3),
|
||||
]);
|
||||
|
||||
let last_layout = LastLayout {
|
||||
visible_range: 0..1,
|
||||
visible_buffer_lines: vec![0],
|
||||
visible_line_byte_offsets: vec![0],
|
||||
visible_top: px(0.),
|
||||
visible_range_offset: 0..0,
|
||||
lines: Rc::new(vec![]),
|
||||
line_height: px(20.),
|
||||
wrap_width: None,
|
||||
line_number_width: px(0.),
|
||||
cursor_bounds: None,
|
||||
text_align: TextAlign::Left,
|
||||
content_width: px(0.),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
line_layout.position_for_index(0, &last_layout, false),
|
||||
Some(point(px(0.), px(0.)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset_to_display_point() {
|
||||
let font = gpui::Font {
|
||||
family: "Arial".into(),
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::Normal,
|
||||
features: FontFeatures::default(),
|
||||
fallbacks: None,
|
||||
};
|
||||
|
||||
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
||||
wrapper.text = Rope::from(
|
||||
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
|
||||
);
|
||||
wrapper.lines = vec![
|
||||
// range: 0..15
|
||||
LineItem {
|
||||
line: Rope::from("Hello, 世界!\r"),
|
||||
wrapped_lines: vec![0..15],
|
||||
},
|
||||
// range: 16..36
|
||||
LineItem {
|
||||
line: Rope::from("This is second line."),
|
||||
wrapped_lines: vec![0..10, 10..20],
|
||||
},
|
||||
// range: 37..56
|
||||
LineItem {
|
||||
line: Rope::from("This is third line."),
|
||||
wrapped_lines: vec![0..9, 9..15, 15..20],
|
||||
},
|
||||
// range: 57..79
|
||||
LineItem {
|
||||
line: Rope::from("这里是第 4 行。"),
|
||||
wrapped_lines: vec![0..22],
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(12),
|
||||
WrapDisplayPoint::new(0, 0, 12)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(15),
|
||||
WrapDisplayPoint::new(0, 0, 15)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(16),
|
||||
WrapDisplayPoint::new(1, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(21),
|
||||
WrapDisplayPoint::new(1, 0, 5)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(27),
|
||||
WrapDisplayPoint::new(2, 1, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(37),
|
||||
WrapDisplayPoint::new(3, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(54),
|
||||
WrapDisplayPoint::new(5, 2, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.offset_to_display_point(59),
|
||||
WrapDisplayPoint::new(6, 0, 2)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(6, 0, 2)),
|
||||
59
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(5, 2, 2)),
|
||||
54
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(3, 0, 0)),
|
||||
37
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(2, 1, 1)),
|
||||
27
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 5)),
|
||||
21
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 0)),
|
||||
16
|
||||
);
|
||||
assert_eq!(
|
||||
wrapper.display_point_to_offset(WrapDisplayPoint::new(0, 0, 15)),
|
||||
15
|
||||
);
|
||||
}
|
||||
}
|
||||
222
crates/ui/src/input/display_map/wrap_map.rs
Normal file
222
crates/ui/src/input/display_map/wrap_map.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user