editor: Improve TextWrapper performance by only rebuild changed lines. (#1266)

Continue #1220 

Now, we can support editing on 100K lines (release mode).



https://github.com/user-attachments/assets/2daccc85-c713-4ddf-b08d-840cb1bb2643
This commit is contained in:
Jason Lee 2025-09-20 02:35:48 +08:00 committed by GitHub
parent 7e9606369e
commit 13fa76e00d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 238 additions and 18 deletions

View file

@ -2282,7 +2282,8 @@ impl EntityInputHandler for InputState {
if let Some(diagnostics) = self.mode.diagnostics_mut() { if let Some(diagnostics) = self.mode.diagnostics_mut() {
diagnostics.reset(&self.text) diagnostics.reset(&self.text)
} }
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper
.update(&self.text, &range, &Rope::from(new_text), false, cx);
self.mode self.mode
.update_highlighter(&range, &self.text, &new_text, true, cx); .update_highlighter(&range, &self.text, &new_text, true, cx);
self.selected_range = (new_offset..new_offset).into(); self.selected_range = (new_offset..new_offset).into();
@ -2331,7 +2332,8 @@ impl EntityInputHandler for InputState {
if let Some(diagnostics) = self.mode.diagnostics_mut() { if let Some(diagnostics) = self.mode.diagnostics_mut() {
diagnostics.reset(&self.text) diagnostics.reset(&self.text)
} }
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper
.update(&self.text, &range, &Rope::from(new_text), false, cx);
self.mode self.mode
.update_highlighter(&range, &self.text, &new_text, true, cx); .update_highlighter(&range, &self.text, &new_text, true, cx);
if new_text.is_empty() { if new_text.is_empty() {
@ -2437,7 +2439,7 @@ impl Focusable for InputState {
impl Render for InputState { impl Render for InputState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper.update_all(&self.text, false, cx);
self.mode self.mode
.update_highlighter(&(0..0), &self.text, "", false, cx); .update_highlighter(&(0..0), &self.text, "", false, cx);

View file

@ -3,7 +3,7 @@ use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels}; use gpui::{App, Font, LineFragment, Pixels};
use rope::Rope; use rope::Rope;
use crate::input::RopeExt as _; use crate::input::RopeExt;
/// A line with soft wrapped lines info. /// A line with soft wrapped lines info.
#[derive(Clone)] #[derive(Clone)]
@ -88,7 +88,7 @@ impl TextWrapper {
} }
self.wrap_width = wrap_width; self.wrap_width = wrap_width;
self.update(&self.text.clone(), true, cx); self.update_all(&self.text.clone(), true, cx);
} }
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) { pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
@ -98,24 +98,77 @@ impl TextWrapper {
self.font = font; self.font = font;
self.font_size = font_size; self.font_size = font_size;
self.update(&self.text.clone(), true, cx); self.update_all(&self.text.clone(), true, cx);
} }
/// Update the text wrapper and recalculate the wrapped lines. /// Update the text wrapper and recalculate the wrapped lines.
/// ///
/// If the `text` is the same as the current text, do nothing. /// If the `text` is the same as the current text, do nothing.
pub(super) fn update(&mut self, text: &Rope, force: bool, cx: &mut App) { ///
if self.text.eq(text) && !force { /// - `changed_text`: The text [`Rope`] that has changed.
return; /// - `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.
let wrap_width = self.wrap_width; /// - `cx`: The application context.
pub(super) fn update(
&mut self,
changed_text: &Rope,
range: &Range<usize>,
new_text: &Rope,
force: bool,
cx: &mut App,
) {
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);
self._update(
changed_text,
range,
new_text,
force,
&mut |line_str, wrap_width| {
line_wrapper
.wrap_line(&[LineFragment::text(line_str)], wrap_width)
.collect()
},
);
}
self.lines.clear(); fn _update<F>(
for line in text.lines() { &mut self,
changed_text: &Rope,
range: &Range<usize>,
new_text: &Rope,
force: bool,
wrap_line: &mut F,
) where
F: FnMut(&str, Pixels) -> Vec<gpui::Boundary>,
{
if self.text.eq(changed_text) && !force {
return;
}
// Remove the old changed lines.
let start_row = self.text.offset_to_point(range.start).row as usize;
let start_row = start_row.min(self.lines.len().saturating_sub(1));
let end_row = self.text.offset_to_point(range.end).row as usize;
let end_row = end_row.min(self.lines.len().saturating_sub(1));
let rows_range = start_row..=end_row;
// To add the new lines.
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
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 as usize;
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;
for line in changed_text.slice(new_range).lines() {
let line_str = line.to_string(); let line_str = line.to_string();
let mut wrapped_lines = vec![]; let mut wrapped_lines = vec![];
let mut prev_boundary_ix = 0; let mut prev_boundary_ix = 0;
@ -123,8 +176,7 @@ impl TextWrapper {
// 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_str)], wrap_width) for boundary in wrap_line(&line_str, wrap_width) {
{
wrapped_lines.push(prev_boundary_ix..boundary.ix); wrapped_lines.push(prev_boundary_ix..boundary.ix);
prev_boundary_ix = boundary.ix; prev_boundary_ix = boundary.ix;
} }
@ -135,13 +187,179 @@ impl TextWrapper {
wrapped_lines.push(prev_boundary_ix..line.len()); wrapped_lines.push(prev_boundary_ix..line.len());
} }
self.lines.push(LineItem { new_lines.push(LineItem {
line: line.clone(), line: line.clone(),
wrapped_lines, wrapped_lines,
}); });
} }
self.text = text.clone(); // dbg!(&new_lines.len());
// dbg!(self.lines.len());
if self.lines.len() == 0 {
self.lines = new_lines;
} else {
self.lines.splice(rows_range, new_lines);
}
// dbg!(self.lines.len());
self.text = changed_text.clone();
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum(); self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
} }
/// Update the text wrapper and recalculate the wrapped lines.
///
/// If the `text` is the same as the current text, do nothing.
pub(crate) fn update_all(&mut self, text: &Rope, force: bool, cx: &mut App) {
self.update(text, &(0..text.len()), &text, force, cx);
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{px, Boundary, FontFeatures, FontStyle, FontWeight};
#[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, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。");
fn fake_wrap_line(_line_str: &str, _wrap_width: Pixels) -> Vec<Boundary> {
vec![]
}
wrapper._update(&text, &(0..text.len()), &text, false, &mut fake_wrap_line);
assert_eq!(wrapper.lines.len(), 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),
false,
&mut fake_wrap_line,
);
assert_eq!(
text.to_string(),
"Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
);
assert_eq!(wrapper.lines.len(), 4);
// 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),
false,
&mut fake_wrap_line,
);
assert_eq!(
text.to_string(),
"AAA, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
);
assert_eq!(wrapper.lines.len(), 4);
// 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(""), false, &mut fake_wrap_line);
assert_eq!(
text.to_string(),
"AAA, 世界!\nThis is third line.\n这里是第 4 行。New text"
);
assert_eq!(wrapper.lines.len(), 3);
// 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),
false,
&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);
// 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),
false,
&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);
// 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),
false,
&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);
// 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),
false,
&mut fake_wrap_line,
);
assert_eq!(text.to_string(), "");
assert_eq!(wrapper.lines.len(), 1);
// 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, false, &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);
}
} }