input: Improve TextWrapper data structure. (#1237)

This commit is contained in:
Jason Lee 2025-09-10 18:52:35 +08:00 committed by GitHub
parent db5ba1e04c
commit 2a918bf4de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 115 additions and 112 deletions

View file

@ -70,6 +70,15 @@ impl From<rope::Point> for LineColumn {
} }
} }
impl From<LineColumn> for rope::Point {
fn from(value: LineColumn) -> Self {
Self {
row: value.line.saturating_sub(1) as u32,
column: value.column.saturating_sub(1) as u32,
}
}
}
impl From<LineColumn> for tree_sitter::Point { impl From<LineColumn> for tree_sitter::Point {
fn from(value: LineColumn) -> Self { fn from(value: LineColumn) -> Self {
Self { Self {

View file

@ -392,7 +392,7 @@ impl TextElement {
let mut visible_range = 0..total_lines; let mut visible_range = 0..total_lines;
let mut line_bottom = px(0.); let mut line_bottom = px(0.);
for (ix, line) in state.text_wrapper.lines.iter().enumerate() { for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
let wrapped_height = (line.wrap_lines + 1) * line_height; let wrapped_height = line.height(line_height);
line_bottom += wrapped_height; line_bottom += wrapped_height;
if line_bottom < -scroll_top { if line_bottom < -scroll_top {
@ -717,35 +717,38 @@ impl Element for TextElement {
.expect("failed to shape text"); .expect("failed to shape text");
// measure.end(); // measure.end();
let longtest_line: SharedString = state let mut longest_line_width = px(0.);
.text if state.mode.is_multi_line() && lines.len() > 1 {
.line(state.text.summary().longest_row as usize) let longtest_line: SharedString = state
.to_string() .text
.into(); .line(state.text.summary().longest_row as usize)
let max_line_width = window .to_string()
.text_system() .into();
.shape_line( longest_line_width = window
longtest_line.clone(), .text_system()
font_size, .shape_line(
&[TextRun { longtest_line.clone(),
len: longtest_line.len(), font_size,
font: style.font(), &[TextRun {
color: gpui::black(), len: longtest_line.len(),
background_color: None, font: style.font(),
underline: None, color: gpui::black(),
strikethrough: None, background_color: None,
}], underline: None,
wrap_width, strikethrough: None,
) }],
.width; wrap_width,
)
.width;
}
let total_wrapped_lines = state.text_wrapper.len(); let total_wrapped_lines = state.text_wrapper.len();
let scroll_size = size( let scroll_size = size(
if max_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width { if longest_line_width + line_number_width + RIGHT_MARGIN > bounds.size.width {
max_line_width + line_number_width + RIGHT_MARGIN longest_line_width + line_number_width + RIGHT_MARGIN
} else { } else {
max_line_width longest_line_width
}, },
(total_wrapped_lines as f32 * line_height).max(bounds.size.height), (total_wrapped_lines as f32 * line_height).max(bounds.size.height),
); );

View file

@ -3,7 +3,6 @@ use crate::{
input::{InputState, LineColumn}, input::{InputState, LineColumn},
}; };
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle}; use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
use itertools::Itertools;
use std::ops::Range; use std::ops::Range;
/// Marker represents a diagnostic message, such as an error or warning, in the code editor. /// Marker represents a diagnostic message, such as an error or warning, in the code editor.
@ -36,41 +35,19 @@ impl Marker {
/// Prepare the marker to convert line, column to byte offsets. /// Prepare the marker to convert line, column to byte offsets.
pub(super) fn prepare(&mut self, state: &InputState) { pub(super) fn prepare(&mut self, state: &InputState) {
let Some(start_line) = state let mut start_point: rope::Point = self.start.into();
.text_wrapper let mut end_point: rope::Point = self.end.into();
.lines
.get(self.start.line.saturating_sub(1))
else {
return;
};
let start_line_str = state.text.slice(start_line.range.clone()); // limit column avoid overflow
let start_line_len = state.text.line_len(start_point.row);
start_point.column = start_point.column.min(start_line_len);
let end_line_len = state.text.line_len(end_point.row);
end_point.column = end_point.column.min(end_line_len);
let Some(end_line) = state let start = state.text.point_to_offset(start_point);
.text_wrapper let end = state.text.point_to_offset(end_point);
.lines
.get(self.end.line.saturating_sub(1))
else {
return;
};
let end_line_str = state.text.slice(end_line.range.clone());
let start_byte = start_line.range.start self.range = Some(start..end);
+ start_line_str
.chars()
.take(self.start.column.saturating_sub(1))
.counts_by(|c| c.len_utf8())
.values()
.sum::<usize>();
let end_byte = end_line.range.start
+ end_line_str
.chars()
.take(self.end.column.saturating_sub(1))
.counts_by(|c| c.len_utf8())
.values()
.sum::<usize>();
self.range = Some(start_byte..end_byte);
} }
} }

View file

@ -105,7 +105,7 @@ impl InputMode {
} }
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) { pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
let wrapped_lines = text_wrapper.wrapped_lines.len(); let wrapped_lines = text_wrapper.len();
self.set_rows(wrapped_lines); self.set_rows(wrapped_lines);
} }

View file

@ -88,9 +88,8 @@ impl std::iter::FusedIterator for RopeLines {}
impl RopeExt for Rope { impl RopeExt for Rope {
fn line(&self, row: usize) -> Rope { fn line(&self, row: usize) -> Rope {
let row = row as u32; let start = self.line_start_offset(row);
let start = self.point_to_offset(Point::new(row, 0)); let end = start + self.line_len(row as u32) as usize;
let end = start + self.line_len(row) as usize;
self.slice(start..end) self.slice(start..end)
} }
@ -145,7 +144,9 @@ mod tests {
assert_eq!(rope.line(1).to_string(), "World\r"); assert_eq!(rope.line(1).to_string(), "World\r");
assert_eq!(rope.line(2).to_string(), "This is a test 中文"); assert_eq!(rope.line(2).to_string(), "This is a test 中文");
assert_eq!(rope.line(3).to_string(), "Rope"); assert_eq!(rope.line(3).to_string(), "Rope");
assert_eq!(rope.line(4).to_string(), "");
// over bounds
assert_eq!(rope.line(6).to_string(), "");
} }
#[test] #[test]

View file

@ -31,7 +31,7 @@ use super::{
}; };
use crate::input::hover_popover::DiagnosticPopover; use crate::input::hover_popover::DiagnosticPopover;
use crate::input::marker::Marker; use crate::input::marker::Marker;
use crate::input::text_wrapper::LineWrap; use crate::input::text_wrapper::LineItem;
use crate::input::{LineColumn, RopeExt as _, Selection}; use crate::input::{LineColumn, RopeExt as _, Selection};
use crate::{history::History, scroll::ScrollbarState, Root}; use crate::{history::History, scroll::ScrollbarState, Root};
@ -687,6 +687,11 @@ impl InputState {
.unwrap_or(self.input_bounds.size.width); .unwrap_or(self.input_bounds.size.width);
self.text_wrapper.set_wrap_width(Some(wrap_width), cx); self.text_wrapper.set_wrap_width(Some(wrap_width), cx);
// Reset scroll to left 0
let mut offset = self.scroll_handle.offset();
offset.x = px(0.);
self.scroll_handle.set_offset(offset);
} else { } else {
self.text_wrapper.set_wrap_width(None, cx); self.text_wrapper.set_wrap_width(None, cx);
} }
@ -725,7 +730,7 @@ impl InputState {
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self { pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
let text: SharedString = value.into(); let text: SharedString = value.into();
self.text = Rope::from(text.as_str()); self.text = Rope::from(text.as_str());
self.text_wrapper.text = self.text.clone(); self.text_wrapper.set_default_text(&self.text);
self self
} }
@ -1722,7 +1727,7 @@ impl InputState {
fn line_origin_with_y_offset( fn line_origin_with_y_offset(
&self, &self,
y_offset: &mut Pixels, y_offset: &mut Pixels,
line: &LineWrap, line: &LineItem,
line_height: Pixels, line_height: Pixels,
) -> Point<Pixels> { ) -> Point<Pixels> {
// NOTE: About line.wrap_boundaries.len() // NOTE: About line.wrap_boundaries.len()
@ -1731,8 +1736,7 @@ impl InputState {
// If have 2 line, the value is 1 // If have 2 line, the value is 1
if self.mode.is_multi_line() { if self.mode.is_multi_line() {
let p = point(px(0.), *y_offset); let p = point(px(0.), *y_offset);
let height = line_height + line.wrap_lines as f32 * line_height; *y_offset += line.height(line_height);
*y_offset = *y_offset + height;
p p
} else { } else {
point(px(0.), px(0.)) point(px(0.), px(0.))

View file

@ -5,48 +5,51 @@ use rope::Rope;
use crate::input::RopeExt as _; use crate::input::RopeExt as _;
#[allow(unused)] /// A line with soft wrapped lines info.
pub(super) struct LineWrap { #[derive(Clone)]
/// The number of soft wrapped lines of this line (Not include first line.) pub(super) struct LineItem {
/// The original line text.
line: Rope,
/// The soft wrapped lines relative byte range (0..line.len) of this line (Include first line).
/// ///
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different /// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
/// like the `window.text_system().shape_text`. So, this value may not equal /// like the `window.text_system().shape_text`. So, this value may not equal
/// the actual rendered lines. /// the actual rendered lines.
pub(super) wrap_lines: usize, wrapped_lines: Vec<Range<usize>>,
/// The range of the line text in the entire text (not includes ending `\n`).
pub(super) range: Range<usize>,
} }
impl LineWrap { impl LineItem {
/// Return the bytes length of this line. /// Get the bytes length of this line.
#[inline]
pub(super) fn len(&self) -> usize { pub(super) fn len(&self) -> usize {
self.range.end - self.range.start self.line.len()
} }
/// Get the total number of lines including wrapped lines. /// Get number of soft wrapped lines of this line (include the first line).
#[inline]
pub(super) fn lines_len(&self) -> usize { pub(super) fn lines_len(&self) -> usize {
self.wrap_lines + 1 self.wrapped_lines.len()
} }
/// Get the height of this line including wrapped lines. /// Get the height of this line item with given line height.
pub(super) fn height(&self, line_height: Pixels) -> Pixels { pub(super) fn height(&self, line_height: Pixels) -> Pixels {
self.lines_len() * line_height self.lines_len() as f32 * line_height
} }
} }
/// Used to prepare the text with soft wrap to be get lines to displayed in the TextArea /// 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 TextArea /// After use lines to calculate the scroll size of the Editor.
pub(super) struct TextWrapper { pub(super) struct TextWrapper {
pub(super) text: Rope, text: Rope,
/// The wrapped lines (Inlucde the first line), value is start and end index of the line. /// Total wrapped lines (Inlucde the first line), value is start and end index of the line.
pub(super) wrapped_lines: Vec<Range<usize>>, soft_lines: usize,
/// The lines by split \n font: Font,
pub(super) lines: Vec<LineWrap>, font_size: Pixels,
pub(super) font: Font,
pub(super) font_size: Pixels,
/// If is none, it means the text is not wrapped /// If is none, it means the text is not wrapped
pub(super) wrap_width: Option<Pixels>, wrap_width: Option<Pixels>,
/// The lines by split \n
pub(super) lines: Vec<LineItem>,
} }
#[allow(unused)] #[allow(unused)]
@ -57,14 +60,26 @@ impl TextWrapper {
font, font,
font_size, font_size,
wrap_width, wrap_width,
wrapped_lines: Vec::new(), soft_lines: 0,
lines: Vec::new(), lines: Vec::new(),
} }
} }
#[inline]
pub(super) fn set_default_text(&mut self, text: &Rope) {
self.text = text.clone();
}
/// Get the total number of lines including wrapped lines. /// Get the total number of lines including wrapped lines.
#[inline]
pub(super) fn len(&self) -> usize { pub(super) fn len(&self) -> usize {
self.wrapped_lines.len() self.soft_lines
}
/// Get the line item by row index.
#[inline]
pub(super) fn line(&self, row: usize) -> Option<&LineItem> {
self.lines.iter().skip(row).next()
} }
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) { pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
@ -94,45 +109,39 @@ impl TextWrapper {
return; return;
} }
let mut wrapped_lines = vec![];
let mut lines = vec![];
let wrap_width = self.wrap_width; let wrap_width = self.wrap_width;
let mut line_wrapper = cx let mut line_wrapper = cx
.text_system() .text_system()
.line_wrapper(self.font.clone(), self.font_size); .line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
self.lines.clear();
for line in text.lines() { for line in text.lines() {
let line = line.to_string(); let line_str = line.to_string();
let mut line_wraps = vec![]; let mut wrapped_lines = vec![];
let mut prev_boundary_ix = 0; let mut prev_boundary_ix = 0;
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap // If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
if let Some(wrap_width) = wrap_width { if let Some(wrap_width) = wrap_width {
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty. // Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) { for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line_str)], wrap_width)
line_wraps.push(prev_boundary_ix..boundary.ix); {
wrapped_lines.push(prev_boundary_ix..boundary.ix);
prev_boundary_ix = boundary.ix; prev_boundary_ix = boundary.ix;
} }
} }
lines.push(LineWrap {
wrap_lines: line_wraps.len(),
range: prev_line_ix..prev_line_ix + line.len(),
});
wrapped_lines.extend(line_wraps);
// Reset of the line // Reset of the line
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 { if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len()); wrapped_lines.push(prev_boundary_ix..line.len());
} }
// +1 for \n self.lines.push(LineItem {
prev_line_ix += line.len() + 1; line: line.clone(),
wrapped_lines,
});
} }
self.text = text.clone(); self.text = text.clone();
self.wrapped_lines = wrapped_lines; self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
self.lines = lines;
} }
} }