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 {
fn from(value: LineColumn) -> Self {
Self {

View file

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

View file

@ -3,7 +3,6 @@ use crate::{
input::{InputState, LineColumn},
};
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
use itertools::Itertools;
use std::ops::Range;
/// 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.
pub(super) fn prepare(&mut self, state: &InputState) {
let Some(start_line) = state
.text_wrapper
.lines
.get(self.start.line.saturating_sub(1))
else {
return;
};
let mut start_point: rope::Point = self.start.into();
let mut end_point: rope::Point = self.end.into();
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
.text_wrapper
.lines
.get(self.end.line.saturating_sub(1))
else {
return;
};
let end_line_str = state.text.slice(end_line.range.clone());
let start = state.text.point_to_offset(start_point);
let end = state.text.point_to_offset(end_point);
let start_byte = start_line.range.start
+ 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);
self.range = Some(start..end);
}
}

View file

@ -105,7 +105,7 @@ impl InputMode {
}
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);
}

View file

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

View file

@ -31,7 +31,7 @@ use super::{
};
use crate::input::hover_popover::DiagnosticPopover;
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::{history::History, scroll::ScrollbarState, Root};
@ -687,6 +687,11 @@ impl InputState {
.unwrap_or(self.input_bounds.size.width);
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 {
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 {
let text: SharedString = value.into();
self.text = Rope::from(text.as_str());
self.text_wrapper.text = self.text.clone();
self.text_wrapper.set_default_text(&self.text);
self
}
@ -1722,7 +1727,7 @@ impl InputState {
fn line_origin_with_y_offset(
&self,
y_offset: &mut Pixels,
line: &LineWrap,
line: &LineItem,
line_height: Pixels,
) -> Point<Pixels> {
// NOTE: About line.wrap_boundaries.len()
@ -1731,8 +1736,7 @@ impl InputState {
// If have 2 line, the value is 1
if self.mode.is_multi_line() {
let p = point(px(0.), *y_offset);
let height = line_height + line.wrap_lines as f32 * line_height;
*y_offset = *y_offset + height;
*y_offset += line.height(line_height);
p
} else {
point(px(0.), px(0.))

View file

@ -5,48 +5,51 @@ use rope::Rope;
use crate::input::RopeExt as _;
#[allow(unused)]
pub(super) struct LineWrap {
/// The number of soft wrapped lines of this line (Not include first line.)
/// A line with soft wrapped lines info.
#[derive(Clone)]
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
/// like the `window.text_system().shape_text`. So, this value may not equal
/// the actual rendered lines.
pub(super) wrap_lines: usize,
/// The range of the line text in the entire text (not includes ending `\n`).
pub(super) range: Range<usize>,
wrapped_lines: Vec<Range<usize>>,
}
impl LineWrap {
/// Return the bytes length of this line.
impl LineItem {
/// Get the bytes length of this line.
#[inline]
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 {
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 {
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) text: Rope,
/// The wrapped lines (Inlucde the first line), value is start and end index of the line.
pub(super) wrapped_lines: Vec<Range<usize>>,
/// The lines by split \n
pub(super) lines: Vec<LineWrap>,
pub(super) font: Font,
pub(super) font_size: Pixels,
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
pub(super) wrap_width: Option<Pixels>,
wrap_width: Option<Pixels>,
/// The lines by split \n
pub(super) lines: Vec<LineItem>,
}
#[allow(unused)]
@ -57,14 +60,26 @@ impl TextWrapper {
font,
font_size,
wrap_width,
wrapped_lines: Vec::new(),
soft_lines: 0,
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.
#[inline]
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) {
@ -94,45 +109,39 @@ impl TextWrapper {
return;
}
let mut wrapped_lines = vec![];
let mut lines = vec![];
let wrap_width = self.wrap_width;
let mut line_wrapper = cx
.text_system()
.line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
self.lines.clear();
for line in text.lines() {
let line = line.to_string();
let mut line_wraps = vec![];
let line_str = line.to_string();
let mut wrapped_lines = vec![];
let mut prev_boundary_ix = 0;
// 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 line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
line_wraps.push(prev_boundary_ix..boundary.ix);
for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line_str)], wrap_width)
{
wrapped_lines.push(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
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
if !line_str[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
wrapped_lines.push(prev_boundary_ix..line.len());
}
// +1 for \n
prev_line_ix += line.len() + 1;
self.lines.push(LineItem {
line: line.clone(),
wrapped_lines,
});
}
self.text = text.clone();
self.wrapped_lines = wrapped_lines;
self.lines = lines;
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
}
}