Skip over folded regions when iterating over multibuffer chunks (#15646)
This commit weaves through new APIs for language::BufferChunks, multi_buffer::MultiBufferChunks and inlay_map::InlayChunks that allow seeking with an upper-bound. This allows us to omit doing syntax highligting and looking up diagnostics for folded ranges. This in turn directly improves performance of assistant panel with large contexts. Release Notes: - Fixed poor performance when editing in the assistant panel after inserting large files using slash commands --------- Co-authored-by: Max <max@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
co-authored by
Max
Max Brunsfeld
parent
390815dd76
commit
ad11d83724
@@ -11,7 +11,7 @@ use std::{
|
||||
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
|
||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -277,6 +277,17 @@ impl FoldMap {
|
||||
"transform tree does not match inlay snapshot's length"
|
||||
);
|
||||
|
||||
let mut prev_transform_isomorphic = false;
|
||||
for transform in self.snapshot.transforms.iter() {
|
||||
if !transform.is_fold() && prev_transform_isomorphic {
|
||||
panic!(
|
||||
"found adjacent isomorphic transforms: {:?}",
|
||||
self.snapshot.transforms.items(&())
|
||||
);
|
||||
}
|
||||
prev_transform_isomorphic = !transform.is_fold();
|
||||
}
|
||||
|
||||
let mut folds = self.snapshot.folds.iter().peekable();
|
||||
while let Some(fold) = folds.next() {
|
||||
if let Some(next_fold) = folds.peek() {
|
||||
@@ -303,11 +314,24 @@ impl FoldMap {
|
||||
} else {
|
||||
let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
|
||||
|
||||
let mut new_transforms = SumTree::new();
|
||||
let mut new_transforms = SumTree::<Transform>::new();
|
||||
let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
|
||||
cursor.seek(&InlayOffset(0), Bias::Right, &());
|
||||
|
||||
while let Some(mut edit) = inlay_edits_iter.next() {
|
||||
if let Some(item) = cursor.item() {
|
||||
if !item.is_fold() {
|
||||
new_transforms.update_last(
|
||||
|transform| {
|
||||
if !transform.is_fold() {
|
||||
transform.summary.add_summary(&item.summary, &());
|
||||
cursor.next(&());
|
||||
}
|
||||
},
|
||||
&(),
|
||||
);
|
||||
}
|
||||
}
|
||||
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
|
||||
edit.new.start -= edit.old.start - *cursor.start();
|
||||
edit.old.start = *cursor.start();
|
||||
@@ -392,16 +416,7 @@ impl FoldMap {
|
||||
if fold_range.start.0 > sum.input.len {
|
||||
let text_summary = inlay_snapshot
|
||||
.text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start);
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
output: text_summary.clone(),
|
||||
input: text_summary,
|
||||
},
|
||||
placeholder: None,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
push_isomorphic(&mut new_transforms, text_summary);
|
||||
}
|
||||
|
||||
if fold_range.end > fold_range.start {
|
||||
@@ -438,32 +453,14 @@ impl FoldMap {
|
||||
if sum.input.len < edit.new.end.0 {
|
||||
let text_summary = inlay_snapshot
|
||||
.text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
output: text_summary.clone(),
|
||||
input: text_summary,
|
||||
},
|
||||
placeholder: None,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
push_isomorphic(&mut new_transforms, text_summary);
|
||||
}
|
||||
}
|
||||
|
||||
new_transforms.append(cursor.suffix(&()), &());
|
||||
if new_transforms.is_empty() {
|
||||
let text_summary = inlay_snapshot.text_summary();
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
output: text_summary.clone(),
|
||||
input: text_summary,
|
||||
},
|
||||
placeholder: None,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
push_isomorphic(&mut new_transforms, text_summary);
|
||||
}
|
||||
|
||||
drop(cursor);
|
||||
@@ -715,17 +712,25 @@ impl FoldSnapshot {
|
||||
highlights: Highlights<'a>,
|
||||
) -> FoldChunks<'a> {
|
||||
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
|
||||
transform_cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let inlay_end = {
|
||||
transform_cursor.seek(&range.end, Bias::Right, &());
|
||||
let overshoot = range.end.0 - transform_cursor.start().0 .0;
|
||||
let inlay_start = {
|
||||
let overshoot = range.start.0 - transform_cursor.start().0 .0;
|
||||
transform_cursor.start().1 + InlayOffset(overshoot)
|
||||
};
|
||||
|
||||
let inlay_start = {
|
||||
transform_cursor.seek(&range.start, Bias::Right, &());
|
||||
let overshoot = range.start.0 - transform_cursor.start().0 .0;
|
||||
let transform_end = transform_cursor.end(&());
|
||||
|
||||
let inlay_end = if transform_cursor
|
||||
.item()
|
||||
.map_or(true, |transform| transform.is_fold())
|
||||
{
|
||||
inlay_start
|
||||
} else if range.end < transform_end.0 {
|
||||
let overshoot = range.end.0 - transform_cursor.start().0 .0;
|
||||
transform_cursor.start().1 + InlayOffset(overshoot)
|
||||
} else {
|
||||
transform_end.1
|
||||
};
|
||||
|
||||
FoldChunks {
|
||||
@@ -737,8 +742,8 @@ impl FoldSnapshot {
|
||||
),
|
||||
inlay_chunk: None,
|
||||
inlay_offset: inlay_start,
|
||||
output_offset: range.start.0,
|
||||
max_output_offset: range.end.0,
|
||||
output_offset: range.start,
|
||||
max_output_offset: range.end,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +788,32 @@ impl FoldSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
|
||||
let mut did_merge = false;
|
||||
transforms.update_last(
|
||||
|last| {
|
||||
if !last.is_fold() {
|
||||
last.summary.input += summary.clone();
|
||||
last.summary.output += summary.clone();
|
||||
did_merge = true;
|
||||
}
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if !did_merge {
|
||||
transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
input: summary.clone(),
|
||||
output: summary,
|
||||
},
|
||||
placeholder: None,
|
||||
},
|
||||
&(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn intersecting_folds<'a, T>(
|
||||
inlay_snapshot: &'a InlaySnapshot,
|
||||
folds: &'a SumTree<Fold>,
|
||||
@@ -1079,8 +1110,8 @@ pub struct FoldChunks<'a> {
|
||||
inlay_chunks: InlayChunks<'a>,
|
||||
inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
|
||||
inlay_offset: InlayOffset,
|
||||
output_offset: usize,
|
||||
max_output_offset: usize,
|
||||
output_offset: FoldOffset,
|
||||
max_output_offset: FoldOffset,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FoldChunks<'a> {
|
||||
@@ -1098,7 +1129,6 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
if let Some(placeholder) = transform.placeholder.as_ref() {
|
||||
self.inlay_chunk.take();
|
||||
self.inlay_offset += InlayOffset(transform.summary.input.len);
|
||||
self.inlay_chunks.seek(self.inlay_offset);
|
||||
|
||||
while self.inlay_offset >= self.transform_cursor.end(&()).1
|
||||
&& self.transform_cursor.item().is_some()
|
||||
@@ -1106,7 +1136,7 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
self.transform_cursor.next(&());
|
||||
}
|
||||
|
||||
self.output_offset += placeholder.text.len();
|
||||
self.output_offset.0 += placeholder.text.len();
|
||||
return Some(Chunk {
|
||||
text: placeholder.text,
|
||||
renderer: Some(placeholder.renderer.clone()),
|
||||
@@ -1114,6 +1144,23 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
// When we reach a non-fold region, seek the underlying text
|
||||
// chunk iterator to the next unfolded range.
|
||||
if self.inlay_offset == self.transform_cursor.start().1
|
||||
&& self.inlay_chunks.offset() != self.inlay_offset
|
||||
{
|
||||
let transform_start = self.transform_cursor.start();
|
||||
let transform_end = self.transform_cursor.end(&());
|
||||
let inlay_end = if self.max_output_offset < transform_end.0 {
|
||||
let overshoot = self.max_output_offset.0 - transform_start.0 .0;
|
||||
transform_start.1 + InlayOffset(overshoot)
|
||||
} else {
|
||||
transform_end.1
|
||||
};
|
||||
|
||||
self.inlay_chunks.seek(self.inlay_offset..inlay_end);
|
||||
}
|
||||
|
||||
// Retrieve a chunk from the current location in the buffer.
|
||||
if self.inlay_chunk.is_none() {
|
||||
let chunk_offset = self.inlay_chunks.offset();
|
||||
@@ -1136,7 +1183,7 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
}
|
||||
|
||||
self.inlay_offset = chunk_end;
|
||||
self.output_offset += chunk.text.len();
|
||||
self.output_offset.0 += chunk.text.len();
|
||||
return Some(chunk);
|
||||
}
|
||||
|
||||
|
||||
@@ -225,14 +225,16 @@ pub struct InlayChunks<'a> {
|
||||
}
|
||||
|
||||
impl<'a> InlayChunks<'a> {
|
||||
pub fn seek(&mut self, offset: InlayOffset) {
|
||||
self.transforms.seek(&offset, Bias::Right, &());
|
||||
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
|
||||
self.transforms.seek(&new_range.start, Bias::Right, &());
|
||||
|
||||
let buffer_offset = self.snapshot.to_buffer_offset(offset);
|
||||
self.buffer_chunks.seek(buffer_offset);
|
||||
let buffer_range = self.snapshot.to_buffer_offset(new_range.start)
|
||||
..self.snapshot.to_buffer_offset(new_range.end);
|
||||
self.buffer_chunks.seek(buffer_range);
|
||||
self.inlay_chunks = None;
|
||||
self.buffer_chunk = None;
|
||||
self.output_offset = offset;
|
||||
self.output_offset = new_range.start;
|
||||
self.max_output_offset = new_range.end;
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> InlayOffset {
|
||||
|
||||
@@ -3668,6 +3668,63 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
);
|
||||
});
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_fold_perf(cx: &mut TestAppContext) {
|
||||
use std::fmt::Write;
|
||||
init_test(cx, |_| {});
|
||||
let mut view = EditorTestContext::new(cx).await;
|
||||
let language_registry = view.language_registry();
|
||||
let language_name = Arc::from("Markdown");
|
||||
let md_language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: Arc::clone(&language_name),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["md".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_md::language()),
|
||||
)
|
||||
.with_highlights_query(
|
||||
r#"
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
language_registry.add(md_language.clone());
|
||||
|
||||
let mut text = String::default();
|
||||
writeln!(&mut text, "start").unwrap();
|
||||
writeln!(&mut text, "```").unwrap();
|
||||
const LINE_COUNT: u32 = 10000;
|
||||
for i in 0..LINE_COUNT {
|
||||
writeln!(&mut text, "{i}").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut text, "```").unwrap();
|
||||
writeln!(&mut text, "end").unwrap();
|
||||
view.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(md_language), cx);
|
||||
});
|
||||
let t0 = Instant::now();
|
||||
_ = view.update_editor(|view, cx| {
|
||||
eprintln!("Text length: {}", text.len());
|
||||
view.set_text(text, cx);
|
||||
eprintln!(">>");
|
||||
view.fold_ranges(
|
||||
vec![(
|
||||
Point::new(1, 0)..Point::new(LINE_COUNT + 2, 3),
|
||||
FoldPlaceholder::test(),
|
||||
)],
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
eprintln!("{:?}", t0.elapsed());
|
||||
eprintln!("<<");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_line_up_down(cx: &mut TestAppContext) {
|
||||
|
||||
Reference in New Issue
Block a user