From 4e96faa6b5fa61f56566d641b32b7b7c843302fb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 26 Feb 2026 23:56:35 -0700 Subject: [PATCH] Update update_path_excerpts --- crates/editor/src/split.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 16 +- crates/multi_buffer/src/path_key.rs | 518 +++++++++++++----------- 3 files changed, 294 insertions(+), 242 deletions(-) diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index cff98f474487b52e55ab3f53bff250de24cf2d80..4155846f401357c558f692f6b45396c3b252156e 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -1193,7 +1193,7 @@ impl SplittableEditor { .collect(); let groups = lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| { - let lhs_result = lhs_multibuffer.update_path_excerpts( + let lhs_result = lhs_multibuffer.u.pdate_path_excerpts( path, base_text_buffer, &base_text_buffer_snapshot, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 461c64a019932a4febc3cc6b2a3e8bd5903f950b..d904c1312eaaddf27081a6d4a0be7aa7ee0e734b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -505,7 +505,6 @@ pub trait ToPoint: 'static + fmt::Debug { struct BufferState { buffer: Entity, - path_key: PathKey, last_version: RefCell, last_non_text_state_update_count: Cell, _subscriptions: [gpui::Subscription; 2], @@ -1178,7 +1177,6 @@ impl MultiBuffer { *buffer_id, BufferState { buffer: buffer_state.buffer.clone(), - path_key: buffer_state.path_key.clone(), last_version: buffer_state.last_version.clone(), last_non_text_state_update_count: buffer_state .last_non_text_state_update_count @@ -7544,6 +7542,20 @@ impl sum_tree::Dimension<'_, ExcerptSummary> for PathKey { } } +impl sum_tree::Dimension<'_, ExcerptSummary> for MultiBufferOffset { + fn zero(_: ::Context<'_>) -> Self { + MultiBufferOffset::ZERO + } + + fn add_summary( + &mut self, + summary: &'_ ExcerptSummary, + _cx: ::Context<'_>, + ) { + self += summary.text.len + } +} + impl sum_tree::ContextLessSummary for ExcerptSummary { fn zero() -> Self { Self::min() diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index c8a4a7a8d1d0eda5de0b4c241f7e832dd55d5475..ace197c9fc7b2c8e31d0603205d02faa0b5f065a 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -1,16 +1,23 @@ -use std::{mem, ops::Range, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + ops::Range, + rc::Rc, + sync::Arc, +}; use collections::HashSet; use gpui::{App, AppContext, Context, Entity}; use itertools::Itertools; use language::{Buffer, BufferSnapshot}; use rope::Point; -use text::{Bias, OffsetRangeExt, locator::Locator}; -use util::{post_inc, rel_path::RelPath}; +use sum_tree::{Dimensions, SumTree}; +use text::{Bias, BufferId, Edit, OffsetRangeExt, Patch}; +use util::rel_path::RelPath; use ztracing::instrument; use crate::{ - Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, build_excerpt_ranges, + Anchor, BufferState, DiffChangeKind, Event, Excerpt, ExcerptRange, ExpandExcerptDirection, + MultiBuffer, MultiBufferOffset, build_excerpt_ranges, }; #[derive(Debug, Clone)] @@ -82,16 +89,6 @@ impl MultiBuffer { .copied() } - pub fn path_for_excerpt(&self, excerpt: ExcerptId) -> Option { - self.paths_by_excerpt.get(&excerpt).cloned() - } - - pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { - if let Some(to_remove) = self.excerpts_by_path.remove(&path) { - self.remove_excerpts(to_remove, cx) - } - } - pub fn buffer_for_path(&self, path: &PathKey, cx: &App) -> Option> { let excerpt_id = self.excerpts_by_path.get(path)?.first()?; let snapshot = self.read(cx); @@ -128,18 +125,19 @@ impl MultiBuffer { cx: &mut Context, ) -> (Vec>, bool) { let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); - - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ) + let ranges: Vec<_> = ranges.into_iter().collect(); + let excerpt_ranges = + build_excerpt_ranges(ranges.clone(), context_line_count, &buffer_snapshot); + + let (new, _) = Self::merge_excerpt_ranges(&excerpt_ranges); + let inserted = + self.set_merged_excerpt_ranges_for_path(path, buffer, &buffer_snapshot, new, cx); + // todo!() move this into the callers that care + let anchors = ranges + .into_iter() + .map(|range| Anchor::range_in_buffer(buffer_snapshot.anchor_range_around(range))) + .collect::>(); + (anchors, inserted) } pub fn set_excerpt_ranges_for_path( @@ -151,15 +149,16 @@ impl MultiBuffer { cx: &mut Context, ) -> (Vec>, bool) { let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - buffer_snapshot, - new, - counts, - cx, - ) + let inserted = + self.set_merged_excerpt_ranges_for_path(path, buffer, buffer_snapshot, new, cx); + // todo!() move this into the callers that care + let anchors = excerpt_ranges + .into_iter() + .map(|range| { + Anchor::range_in_buffer(buffer_snapshot.anchor_range_around(range.primary)) + }) + .collect::>(); + (anchors, inserted) } pub fn set_anchored_excerpts_for_path( @@ -175,28 +174,29 @@ impl MultiBuffer { let mut app = cx.to_async(); async move { let snapshot = buffer_snapshot.clone(); - let (excerpt_ranges, new, counts) = app + let (ranges, merged_excerpt_ranges) = app .background_spawn(async move { - let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); + let point_ranges = ranges.iter().map(|range| range.to_point(&snapshot)); let excerpt_ranges = - build_excerpt_ranges(ranges, context_line_count, &snapshot); - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - (excerpt_ranges, new, counts) + build_excerpt_ranges(point_ranges, context_line_count, &snapshot); + let (new, _) = Self::merge_excerpt_ranges(&excerpt_ranges); + (ranges, new) }) .await; multi_buffer .update(&mut app, move |multi_buffer, cx| { - let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( + multi_buffer.set_merged_excerpt_ranges_for_path( path_key, buffer, - excerpt_ranges, &buffer_snapshot, - new, - counts, + merged_excerpt_ranges, cx, ); ranges + .into_iter() + .map(|range| Anchor::range_in_buffer(range)) + .collect() }) .ok() .unwrap_or_default() @@ -296,229 +296,269 @@ impl MultiBuffer { &mut self, path: PathKey, buffer: Entity, - ranges: Vec>, buffer_snapshot: &BufferSnapshot, new: Vec>, - counts: Vec, cx: &mut Context, - ) -> (Vec>, bool) { - let insert_result = self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); - - let mut result = Vec::new(); - let mut ranges = ranges.into_iter(); - for (excerpt_id, range_count) in insert_result - .excerpt_ids + ) -> bool { + let anchor_ranges = new .into_iter() - .zip(counts.into_iter()) - { - for range in ranges.by_ref().take(range_count) { - let range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - ); - result.push(range) - } - } - (result, insert_result.added_new_excerpt) + .map(|r| ExcerptRange { + context: buffer_snapshot.anchor_range_around(r.context), + primary: buffer_snapshot.anchor_range_around(r.primary), + }) + .collect::>(); + self.update_path_excerpts(path, buffer, buffer_snapshot, &anchor_ranges, cx) } - pub fn update_path_excerpts( + pub fn update_path_excerpts<'a>( &mut self, - path: PathKey, + path_key: PathKey, buffer: Entity, buffer_snapshot: &BufferSnapshot, - new: Vec>, + to_insert: &Vec>, cx: &mut Context, - ) -> PathExcerptInsertResult { - let mut insert_after = self - .excerpts_by_path - .range(..path.clone()) - .next_back() - .and_then(|(_, value)| value.last().copied()) - .unwrap_or(ExcerptId::min()); - - let existing = self - .excerpts_by_path - .get(&path) - .cloned() - .unwrap_or_default(); - let mut new_iter = new.into_iter().peekable(); - let mut existing_iter = existing.into_iter().peekable(); - - let mut excerpt_ids = Vec::new(); - let mut to_remove = Vec::new(); - let mut to_insert: Vec<(ExcerptId, ExcerptRange)> = Vec::new(); - let mut added_a_new_excerpt = false; - let snapshot = self.snapshot(cx); + ) -> bool { + if to_insert.len() == 0 { + self.remove_excerpts_for_path(path_key, cx); + if let Some(old_path_key) = self + .snapshot(cx) + .path_for_buffer(buffer_snapshot.remote_id()) + && old_path_key != path_key + { + self.remove_excerpts_for_path(old_path_key, cx); + } - let mut next_excerpt_id = - if let Some(last_entry) = self.snapshot.get_mut().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; + return false; + } + assert_eq!(self.history.transaction_depth(), 0); + self.sync_mut(cx); + + let buffer_snapshot = buffer.read(cx).snapshot(); + let buffer_id = buffer_snapshot.remote_id(); + let buffer_state = self.buffers.entry(buffer_id).or_insert_with(|| { + self.buffer_changed_since_sync.replace(true); + buffer.update(cx, |buffer, _| { + buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); + }); + BufferState { + last_version: RefCell::new(buffer_snapshot.version().clone()), + last_non_text_state_update_count: Cell::new( + buffer_snapshot.non_text_state_update_count(), + ), + _subscriptions: [ + cx.observe(&buffer, |_, _, cx| cx.notify()), + cx.subscribe(&buffer, Self::on_buffer_event), + ], + buffer: buffer.clone(), + } + }); - let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); + let mut snapshot = self.snapshot.get_mut(); + let mut cursor = snapshot + .excerpts + .cursor::>(()); + let mut new_excerpts = SumTree::new(()); + + let to_insert = to_insert.iter().peekable(); + let mut patch = Patch::empty(); + let mut added_new_excerpt = false; + + let old_path_key = snapshot.path_keys.insert_or_replace(buffer_id, path_key); + // handle the case where the buffer's path key has changed by + // removing any old excerpts for the buffer + if let Some(old_path_key) = old_path_key + && old_path_key < path_key + { + new_excerpts.append(cursor.slice(&old_path_key, Bias::Left), ()); + let before = cursor.position.1; + cursor.seek_forward(&old_path_key, Bias::Right); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); + } - let mut excerpts_cursor = snapshot.excerpts.cursor::>(()); - excerpts_cursor.next(); + new_excerpts.append(cursor.slice(&path_key, Bias::Left), ()); - loop { - let existing = if let Some(&existing_id) = existing_iter.peek() { - let locator = snapshot.excerpt_locator_for_id(existing_id); - excerpts_cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts_cursor.item() { - if excerpt.buffer_id != buffer_snapshot.remote_id() { - to_remove.push(existing_id); - existing_iter.next(); - continue; - } - Some((existing_id, excerpt.range.context.to_point(buffer_snapshot))) - } else { - None - } - } else { - None + // handle the case where the path key used to be associated + // with a different buffer by removing its excerpts. + if let Some(excerpt) = cursor.item() + && excerpt.path_key == path_key + && excerpt.buffer.remote_id() != buffer_id + { + let before = cursor.position.1; + cursor.seek_forward(&path_key, Bias::Right); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); + } + + let buffer_snapshot = Arc::new(buffer_snapshot); + while let Some(excerpt) = cursor.item() + && excerpt.path_key == path_key + { + assert_eq!(excerpt.buffer.remote_id(), buffer_id); + let Some(next_excerpt) = to_insert.peek() else { + break; }; + if &excerpt.range == next_excerpt { + new_excerpts.push(excerpt.clone(), ()); + to_insert.next(); + cursor.next(); + continue; + } - let new = new_iter.peek(); - // Try to merge the next new range or existing excerpt into the last - // queued insert. - if let Some((last_id, last)) = to_insert.last_mut() { - // Next new range overlaps the last queued insert: absorb it by - // extending the insert's end. - if let Some(new) = new - && last.context.end >= new.context.start - { - last.context.end = last.context.end.max(new.context.end); - excerpt_ids.push(*last_id); - new_iter.next(); - continue; - } - // Next existing excerpt overlaps the last queued insert: absorb - // it by extending the insert's end, and record the existing - // excerpt as replaced so anchors in it resolve to the new one. - if let Some((existing_id, existing_range)) = &existing - && last.context.end >= existing_range.start - { - last.context.end = last.context.end.max(existing_range.end); - to_remove.push(*existing_id); - Arc::make_mut(&mut self.snapshot.get_mut().replaced_excerpts) - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } + if excerpt + .range + .context + .start + .cmp(&next_excerpt.context.start, &buffer_snapshot) + .is_le() + { + let before = cursor.position.1; + cursor.next(); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); + } else { + let before = new_excerpts.summary().text.len; + let next_excerpt = to_insert.next().unwrap(); + added_new_excerpt = true; + new_excerpts.push( + Excerpt::new( + path_key.clone(), + buffer_snapshot.clone(), + next_excerpt.clone(), + to_insert.peek().is_some(), + ), + (), + ); + patch.push(Edit { + old: cursor.position.1..cursor.position.1, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); } + } - match (new, existing) { - (None, None) => break, + // remove any further trailing excerpts + let before = cursor.position.1; + cursor.seek_forward(&path_key, Bias::Right); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); - // No more new ranges; remove the remaining existing excerpt. - (None, Some((existing_id, _))) => { - existing_iter.next(); - to_remove.push(existing_id); - } + // handle the case where the buffer's path key has changed by + // removing any old excerpts for the buffer + if let Some(old_path_key) = old_path_key + && old_path_key > path_key + { + new_excerpts.append(cursor.slice(&old_path_key, Bias::Left), ()); + let before = cursor.position.1; + cursor.seek_forward(&old_path_key, Bias::Right); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); + } - // No more existing excerpts; queue the new range for insertion. - (Some(_), None) => { - added_a_new_excerpt = true; - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - } + let suffix = cursor.suffix(); + let changed_trailing_excerpt = suffix.is_empty(); + new_excerpts.append(suffix, ()); + snapshot.excerpts = new_excerpts; + if changed_trailing_excerpt { + snapshot.trailing_excerpt_update_count += 1; + } - // Existing excerpt ends before the new range starts, so it - // has no corresponding new range and must be removed. Flush - // pending inserts and advance `insert_after` past it so that - // future inserts receive locators *after* this excerpt's - // locator, preserving forward ordering. - (Some(new), Some((_, existing_range))) - if existing_range.end < new.context.start => - { - self.insert_excerpts_with_ids_after( - insert_after, - buffer.clone(), - mem::take(&mut to_insert), - cx, - ); - insert_after = existing_iter.next().unwrap(); - to_remove.push(insert_after); - } - // New range ends before the existing excerpt starts, so the - // new range has no corresponding existing excerpt. Queue it - // for insertion at the current `insert_after` position - // (before the existing excerpt), which is the correct - // spatial ordering. - (Some(new), Some((_, existing_range))) - if existing_range.start > new.context.end => - { - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - } - // Exact match: keep the existing excerpt in place, flush - // any pending inserts before it, and use it as the new - // `insert_after` anchor. - (Some(new), Some((_, existing_range))) - if existing_range.start == new.context.start - && existing_range.end == new.context.end => - { - self.insert_excerpts_with_ids_after( - insert_after, - buffer.clone(), - mem::take(&mut to_insert), - cx, - ); - insert_after = existing_iter.next().unwrap(); - excerpt_ids.push(insert_after); - new_iter.next(); - } + let edits = Self::sync_diff_transforms( + &mut snapshot, + patch.into_inner(), + DiffChangeKind::BufferEdited, + ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } - // Partial overlap: replace the existing excerpt with a new - // one whose range is the union of both, and record the - // replacement so that anchors in the old excerpt resolve to - // the new one. - (Some(_), Some((_, existing_range))) => { - let existing_id = existing_iter.next().unwrap(); - let new_id = next_excerpt_id(); - Arc::make_mut(&mut self.snapshot.get_mut().replaced_excerpts) - .insert(existing_id, new_id); - to_remove.push(existing_id); - let mut range = new_iter.next().unwrap(); - range.context.start = range.context.start.min(existing_range.start); - range.context.end = range.context.end.max(existing_range.end); - excerpt_ids.push(new_id); - to_insert.push((new_id, range)); - } - }; + cx.emit(Event::Edited { + edited_buffer: None, + }); + cx.emit(Event::BufferUpdated { + buffer, + path_key, + ranges: new, + }); + cx.notify(); + + added_new_excerpt + } + + pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { + let mut patch = Patch::empty(); + + let mut snapshot = self.snapshot.get_mut(); + let mut cursor = snapshot + .excerpts + .cursor::>(()); + let mut new_excerpts = SumTree::new(()); + + if let Some(old_path_key) = old_path_key + && old_path_key < path_key + { + new_excerpts.append(cursor.slice(&old_path_key, Bias::Left), ()); + let before = cursor.position.1; + cursor.seek_forward(&old_path_key, Bias::Right); + let after = cursor.position.1; + patch.push(Edit { + old: before..after, + new: new_excerpts.summary().text.len..new_excerpts.summary().text.len, + }); } - self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx); - // todo(lw): There is a logic bug somewhere that causes the to_remove vector to be not ordered correctly - to_remove.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)); - self.remove_excerpts(to_remove, cx); + let suffix = cursor.suffix(); + let changed_trailing_excerpt = suffix.is_empty(); + new_excerpts.append(suffix, ()); - if excerpt_ids.is_empty() { - self.excerpts_by_path.remove(&path); - } else { - let snapshot = &*self.snapshot.get_mut(); - let excerpt_ids = excerpt_ids - .iter() - .dedup() - .cloned() - // todo(lw): There is a logic bug somewhere that causes excerpt_ids to not necessarily be in order by locator - .sorted_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)) - .collect(); - for &excerpt_id in &excerpt_ids { - self.paths_by_excerpt.insert(excerpt_id, path.clone()); + for buffer_id in removed_excerpts_for_buffers { + match self.buffers.get(&buffer_id) { + Some(buffer_state) => { + snapshot + .buffer_locators + .insert(buffer_id, buffer_state.excerpts.iter().cloned().collect()); + } + None => { + snapshot.buffer_locators.remove(&buffer_id); + } } - self.excerpts_by_path.insert(path, excerpt_ids); + } + snapshot.excerpts = new_excerpts; + if changed_trailing_excerpt { + snapshot.trailing_excerpt_update_count += 1; } - PathExcerptInsertResult { - excerpt_ids, - added_new_excerpt: added_a_new_excerpt, + let edits = Self::sync_diff_transforms( + &mut snapshot, + patch.into_inner(), + DiffChangeKind::BufferEdited, + ); + if !edits.is_empty() { + self.subscriptions.publish(edits); } + + cx.emit(Event::Edited { + edited_buffer: None, + }); + cx.emit(Event::BufferUpdated { + buffer, + path_key, + ranges: new, + }); + cx.notify(); } }