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:
parent
7e9606369e
commit
13fa76e00d
2 changed files with 238 additions and 18 deletions
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue