use std::ops::Range; use gpui::{ App, Font, Half, 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>, } 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, /// 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, _initialized: bool, } #[allow(unused)] impl TextWrapper { pub(crate) fn new(font: Font, font_size: Pixels, wrap_width: Option) -> 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.get(row) } pub(crate) fn set_wrap_width(&mut self, wrap_width: Option, 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, 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( &mut self, changed_text: &Rope, range: &Range, new_text: &Rope, wrap_line: &mut F, ) where F: FnMut(&str, Pixels) -> Vec, { // 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.is_empty() { 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::(); 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); 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(); } 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, /// 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) -> 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> { 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, last_layout: &LastLayout, ) -> Option { 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, last_layout: &LastLayout, ) -> Option { 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 { size(self.longest_width, self.wrapped_lines.len() * line_height) } pub(crate) fn paint( &self, pos: Point, line_height: Pixels, text_align: TextAlign, align_width: Option, 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); } } } }