multi_buffer: Don't eagerly clone BufferSnapshot in range_to_buffer_ranges (#59190)

Both cloning and dropping of these has quite a bit of overhead (despite
them being snapshots), so avoid where possible, especially in display
map syncing

Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth 2026-06-12 14:27:28 +02:00 committed by GitHub
parent 992f395c3d
commit b6c7496aea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 78 additions and 85 deletions

View file

@ -1245,7 +1245,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
let path_prefix = if include_root_name {
worktree.read(cx).root_name().into()
} else {
RelPath::empty().into()
RelPath::empty_arc()
};
Match::File(FileMatch {
mat: fuzzy::PathMatch {
@ -2143,9 +2143,9 @@ pub(crate) fn search_files(
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|wt| wt.read(cx).root_name().into())
.unwrap_or_else(|| RelPath::empty().into())
.unwrap_or_else(|| RelPath::empty_arc())
} else {
RelPath::empty().into()
RelPath::empty_arc()
};
FileMatch {
@ -2167,7 +2167,7 @@ pub(crate) fn search_files(
let path_prefix: Arc<RelPath> = if include_root_name {
worktree.root_name().into()
} else {
RelPath::empty().into()
RelPath::empty_arc()
};
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {

View file

@ -184,6 +184,9 @@ impl Editor {
.range_to_buffer_ranges(visible_range)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.map(|(buffer_snapshot, buffer_offset_range, excerpt_range)| {
(buffer_snapshot.clone(), buffer_offset_range, excerpt_range)
})
.collect()
}

View file

@ -773,6 +773,8 @@ impl BlockMap {
edits = self.deferred_edits.take().compose(edits);
let max_point = wrap_snapshot.max_point();
// Handle changing the last excerpt if it is empty.
if buffer.trailing_excerpt_update_count()
!= self
@ -781,7 +783,6 @@ impl BlockMap {
.buffer_snapshot()
.trailing_excerpt_update_count()
{
let max_point = wrap_snapshot.max_point();
let edit_start = wrap_snapshot.prev_row_boundary(max_point);
let edit_end = max_point.row() + WrapRow(1); // this is end of file
edits = edits.compose([WrapEdit {
@ -829,7 +830,7 @@ impl BlockMap {
let mut my_start = wrap_snapshot.make_wrap_point(my_start, Bias::Left);
let mut my_end = wrap_snapshot.make_wrap_point(my_end, Bias::Left);
// TODO(split-diff) should use trailing_excerpt_update_count for the second case
if my_end.column() > 0 || my_end == wrap_snapshot.max_point() {
if my_end.column() > 0 || my_end == max_point {
*my_end.row_mut() += 1;
*my_end.column_mut() = 0;
}
@ -837,7 +838,7 @@ impl BlockMap {
// Empty edits won't survive Patch::compose, but we still need to make sure
// we recompute spacers when we get them.
if my_start.row() == my_end.row() {
if my_end.row() <= wrap_snapshot.max_point().row() {
if my_end.row() <= max_point.row() {
*my_end.row_mut() += 1;
*my_end.column_mut() = 0;
} else if my_start.row() > WrapRow(0) {
@ -1006,7 +1007,7 @@ impl BlockMap {
};
let end_bound;
let end_block_ix = if new_end > wrap_snapshot.max_point().row() {
let end_block_ix = if new_end > max_point.row() {
end_bound = Bound::Unbounded;
self.custom_blocks.len()
} else {

View file

@ -72,6 +72,9 @@ impl TabMap {
old_snapshot.tab_size = tab_size;
return (old_snapshot.clone(), vec![]);
}
let old_fold_max_point = old_snapshot.fold_snapshot.max_point();
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
@ -89,11 +92,9 @@ impl TabMap {
// expansion boundary (transitioning between expanded and non-expanded).
for fold_edit in &mut fold_edits {
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let old_end_row_successor_offset = cmp::min(
FoldPoint::new(old_end.row() + 1, 0),
old_snapshot.fold_snapshot.max_point(),
)
.to_offset(&old_snapshot.fold_snapshot);
let old_end_row_successor_offset =
cmp::min(FoldPoint::new(old_end.row() + 1, 0), old_fold_max_point)
.to_offset(&old_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&fold_snapshot);
let mut offset_from_edit = 0;
@ -211,7 +212,7 @@ impl std::ops::Deref for TabSnapshot {
}
impl TabSnapshot {
#[ztracing::instrument(skip_all)]
#[inline]
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
}

View file

@ -8,7 +8,7 @@ use super::{
use futures_lite::future::yield_now;
use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Task};
use language::{LanguageAwareStyling, Point};
use multi_buffer::{MultiBufferSnapshot, RowInfo};
use multi_buffer::RowInfo;
use std::{cmp, collections::VecDeque, mem, ops::Range, sync::LazyLock, time::Duration};
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
use text::Patch;
@ -381,11 +381,6 @@ impl WrapSnapshot {
}
}
#[ztracing::instrument(skip_all)]
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.tab_snapshot.buffer_snapshot()
}
#[ztracing::instrument(skip_all)]
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> WrapPatch {
let mut new_transforms;

View file

@ -534,13 +534,15 @@ pub struct EditorStyle {
impl Default for EditorStyle {
fn default() -> Self {
static NONE_SYNTAX: std::sync::LazyLock<Arc<SyntaxTheme>> =
std::sync::LazyLock::new(|| Arc::new(SyntaxTheme::default()));
Self {
background: Hsla::default(),
border: Hsla::default(),
local_player: PlayerColor::default(),
text: TextStyle::default(),
scrollbar_width: Pixels::default(),
syntax: Default::default(),
syntax: NONE_SYNTAX.clone(),
// HACK: Status colors don't have a real default.
// We should look into removing the status colors from the editor
// style and retrieve them directly from the theme.

View file

@ -2376,7 +2376,7 @@ impl NewlineConfig {
.range_to_buffer_ranges(range.start..range.end)
.as_slice()
{
[(buffer_snapshot, range, _)] => (buffer_snapshot.clone(), range.clone()),
[(buffer_snapshot, range, _)] => (*buffer_snapshot, range.clone()),
_ => return false,
};
let pair = {

View file

@ -2255,7 +2255,7 @@ mod tests {
#[gpui::test]
fn test_path_for_file(cx: &mut App) {
let file: Arc<dyn language::File> = Arc::new(TestFile {
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
root_name: String::new(),
local_root: None,
});

View file

@ -187,15 +187,15 @@ fn patches_for_range<F>(
where
F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
{
struct PendingExcerpt {
source_buffer_snapshot: language::BufferSnapshot,
struct PendingExcerpt<'a> {
source_buffer_snapshot: &'a language::BufferSnapshot,
source_excerpt_range: ExcerptRange<text::Anchor>,
buffer_point_range: Range<Point>,
}
let mut result = Vec::new();
let mut current_buffer_id: Option<BufferId> = None;
let mut pending_excerpts: Vec<PendingExcerpt> = Vec::new();
let mut pending_excerpts: Vec<PendingExcerpt<'_>> = Vec::new();
let mut union_context_start: Option<Point> = None;
let mut union_context_end: Option<Point> = None;

View file

@ -764,7 +764,7 @@ fn matching_history_items<'a>(
candidates_paths.remove_entry(&project_path).or_else(|| {
candidates_paths.remove_entry(&ProjectPath {
worktree_id,
path: RelPath::empty().into_arc(),
path: RelPath::empty_arc(),
})
})?;
// Key with path_match.path so the deduplication check in push_new_matches
@ -824,7 +824,7 @@ fn project_path_for_search_match(
.worktree_for_id(worktree_id, cx)
.is_some_and(|worktree| worktree.read(cx).is_single_file())
{
RelPath::empty().into_arc()
RelPath::empty_arc()
} else {
path_match.path.clone()
};
@ -1457,7 +1457,7 @@ impl FileFinderDelegate {
positions: Vec::new(),
worktree_id: worktree.read(cx).id().to_usize(),
path: relative_path,
path_prefix: RelPath::empty().into(),
path_prefix: RelPath::empty_arc(),
is_dir: false, // File finder doesn't support directories
distance_to_relative_ancestor: usize::MAX,
}));

View file

@ -2641,7 +2641,7 @@ async fn test_single_file_search_result_split_open(cx: &mut gpui::TestAppContext
active_editor.read(cx).active_project_path(cx),
Some(ProjectPath {
worktree_id,
path: RelPath::empty().into_arc(),
path: RelPath::empty_arc(),
}),
"Should split-open the single-file worktree root with an empty relative path"
);

View file

@ -605,7 +605,7 @@ mod tests {
worktree_id: 0,
positions: positions.clone(),
path: candidate.path.into(),
path_prefix: RelPath::empty().into(),
path_prefix: RelPath::empty_arc(),
distance_to_relative_ancestor: usize::MAX,
is_dir: false,
},

View file

@ -116,11 +116,7 @@ pub fn match_fixed_path_set(
(worktree_root_name, path_prefix_chars, lowercase_pfx)
}
None => (
RelPath::empty().into(),
Default::default(),
Default::default(),
),
None => (RelPath::empty_arc(), Default::default(), Default::default()),
};
matcher.match_candidates(

View file

@ -207,7 +207,7 @@ fn path_match_helper<'a>(
candidate.path.into()
},
path_prefix: if root_is_file {
RelPath::empty().into()
RelPath::empty_arc()
} else {
Arc::clone(path_prefix)
},
@ -239,7 +239,7 @@ pub fn match_fixed_path_set(
let root_is_file = worktree_root_name.is_some() && candidates.iter().all(|c| c.path.is_empty());
let path_prefix = worktree_root_name.unwrap_or_else(|| RelPath::empty().into());
let path_prefix = worktree_root_name.unwrap_or_else(|| RelPath::empty_arc());
let mut results = Vec::new();

View file

@ -466,7 +466,7 @@ impl DivInspector {
let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
worktree_id: worktree.id(),
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
});
let buffer = project

View file

@ -889,7 +889,7 @@ impl ContextProvider for PythonContextProvider {
.as_ref()
.and_then(|f| f.path().parent())
.map(Arc::from)
.unwrap_or_else(|| RelPath::empty().into());
.unwrap_or_else(|| RelPath::empty_arc());
toolchains
.active_toolchain(worktree_id, file_path, "Python".into(), cx)

View file

@ -3610,7 +3610,7 @@ impl MultiBufferSnapshot {
&self,
range: Range<T>,
) -> Vec<(
BufferSnapshot,
&BufferSnapshot,
Range<BufferOffset>,
ExcerptRange<text::Anchor>,
)> {
@ -3621,7 +3621,7 @@ impl MultiBufferSnapshot {
cursor.seek(&start);
let mut result: Vec<(
BufferSnapshot,
&BufferSnapshot,
Range<BufferOffset>,
ExcerptRange<text::Anchor>,
)> = Vec::new();
@ -3653,7 +3653,7 @@ impl MultiBufferSnapshot {
{
prev.1.end = end;
} else {
result.push((region.buffer.clone(), start..end, excerpt_range));
result.push((region.buffer, start..end, excerpt_range));
}
}
cursor.next();
@ -3676,11 +3676,7 @@ impl MultiBufferSnapshot {
|| prev_excerpt.context.start != excerpt_range.context.start
})
{
result.push((
buffer_snapshot.clone(),
buffer_offset..buffer_offset,
excerpt_range,
));
result.push((buffer_snapshot, buffer_offset..buffer_offset, excerpt_range));
}
}

View file

@ -26,14 +26,14 @@ impl PathKey {
pub fn min() -> Self {
Self {
sort_prefix: None,
path: RelPath::empty().into_arc(),
path: RelPath::empty_arc(),
}
}
pub fn sorted(sort_prefix: u64) -> Self {
Self {
sort_prefix: Some(sort_prefix),
path: RelPath::empty().into_arc(),
path: RelPath::empty_arc(),
}
}
pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {

View file

@ -186,7 +186,7 @@ impl ManifestTree {
.and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx))
.unwrap_or_else(|| ProjectPath {
worktree_id,
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
})
}

View file

@ -338,7 +338,7 @@ impl LanguageServerTree {
.entry(worktree_id)
.or_default()
.roots
.entry(RelPath::empty().into())
.entry(RelPath::empty_arc())
.or_default()
.entry(node.disposition.server_name.clone())
.or_insert_with(|| (node, BTreeSet::new()))

View file

@ -453,7 +453,7 @@ impl ProjectPath {
pub fn root_path(worktree_id: WorktreeId) -> Self {
Self {
worktree_id,
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
}
}
@ -6385,7 +6385,7 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
self.snapshot.root_name().into()
} else {
RelPath::empty().into()
RelPath::empty_arc()
}
}
@ -6460,7 +6460,7 @@ impl<'a> fuzzy_nucleo::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
if self.snapshot.root_entry().is_some_and(|e| e.is_file()) || self.include_root_name {
self.snapshot.root_name().into()
} else {
RelPath::empty().into()
RelPath::empty_arc()
}
}
fn root_is_file(&self) -> bool {

View file

@ -1202,7 +1202,7 @@ mod tests {
let other_worktree = WorktreeId::from_usize(2);
let kind = TaskSourceKind::Worktree {
id: task_worktree,
directory_in_worktree: RelPath::empty().into_arc(),
directory_in_worktree: RelPath::empty_arc(),
id_base: Cow::Borrowed("worktree"),
};

View file

@ -265,7 +265,7 @@ impl ToolchainStore {
let path = if let Some(path) = envelope.payload.path {
RelPath::from_proto(&path)?
} else {
RelPath::empty().into()
RelPath::empty_arc()
};
Ok(this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx))
})?

View file

@ -433,7 +433,7 @@ impl WorktreeStore {
Task::ready(Ok((tree, relative_path)))
} else {
let worktree = self.create_worktree(abs_path, visible, cx);
cx.background_spawn(async move { Ok((worktree.await?, RelPath::empty().into())) })
cx.background_spawn(async move { Ok((worktree.await?, RelPath::empty_arc())) })
}
}

View file

@ -426,7 +426,7 @@ pub fn python_env_kernel_specifications(
let toolchains = project.read(cx).available_toolchains(
ProjectPath {
worktree_id,
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
},
python_language,
cx,

View file

@ -178,7 +178,7 @@ impl ReplStore {
let active_toolchain = project.read(cx).active_toolchain(
ProjectPath {
worktree_id,
path: RelPath::empty().into(),
path: RelPath::empty_arc(),
},
LanguageName::new_static("Python"),
cx,

View file

@ -1169,10 +1169,10 @@ impl SettingsStore {
) -> impl '_ + Iterator<Item = (Arc<RelPath>, &ProjectSettingsContent)> {
self.local_settings
.range(
(root_id, RelPath::empty().into())
(root_id, RelPath::empty_arc())
..(
WorktreeId::from_usize(root_id.to_usize() + 1),
RelPath::empty().into(),
RelPath::empty_arc(),
),
)
.map(|((_, path), content)| (path.clone(), &content.project))
@ -2566,7 +2566,7 @@ mod tests {
store
.set_user_settings(r#"{"preferred_line_length": 0}"#, cx)
.unwrap();
let local = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
let local = (WorktreeId::from_usize(0), RelPath::empty_arc());
store
.set_local_settings(
local.0,
@ -2626,7 +2626,7 @@ mod tests {
store.register_setting::<DefaultLanguageSettings>();
store.register_setting::<AutoUpdateSetting>();
let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
let local_1 = (WorktreeId::from_usize(0), RelPath::empty_arc());
let local_1_child = (
WorktreeId::from_usize(0),
@ -2638,7 +2638,7 @@ mod tests {
.into_arc(),
);
let local_2 = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
let local_2 = (WorktreeId::from_usize(1), RelPath::empty_arc());
let local_2_child = (
WorktreeId::from_usize(1),
RelPath::new(
@ -2759,11 +2759,11 @@ mod tests {
let mut store = SettingsStore::new(cx, &test_settings());
store.register_setting::<DefaultLanguageSettings>();
let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
let wt0_root = (WorktreeId::from_usize(0), RelPath::empty_arc());
let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc());
let wt0_child2 = (WorktreeId::from_usize(0), rel_path("child2").into_arc());
let wt1_root = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
let wt1_root = (WorktreeId::from_usize(1), RelPath::empty_arc());
let wt1_subdir = (WorktreeId::from_usize(1), rel_path("subdir").into_arc());
fn get(content: &SettingsContent) -> &Option<u32> {
@ -2881,15 +2881,13 @@ mod tests {
#[test]
fn test_file_ord() {
let wt0_root =
SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty().into_arc()));
let wt0_root = SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty_arc()));
let wt0_child1 =
SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child1").into_arc()));
let wt0_child2 =
SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child2").into_arc()));
let wt1_root =
SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty().into_arc()));
let wt1_root = SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty_arc()));
let wt1_subdir =
SettingsFile::Project((WorktreeId::from_usize(1), rel_path("subdir").into_arc()));

View file

@ -2661,7 +2661,7 @@ mod tests {
let entry = cx
.update(|cx| {
wt.update(cx, |wt, cx| {
wt.create_entry(RelPath::empty().into(), is_dir, None, cx)
wt.create_entry(RelPath::empty_arc(), is_dir, None, cx)
})
})
.await

View file

@ -36,6 +36,12 @@ impl RelPath {
Self::new_unchecked("")
}
/// Creates an empty [`RelPath`].
pub fn empty_arc() -> Arc<Self> {
static EMPTY: std::sync::OnceLock<Arc<RelPath>> = std::sync::OnceLock::new();
EMPTY.get_or_init(|| Arc::from(Self::empty())).clone()
}
/// Converts a path with a given style into a [`RelPath`].
///
/// Returns an error if the path is absolute, or is not valid unicode.

View file

@ -215,7 +215,7 @@ impl WorkDirectory {
fn path_key(&self) -> PathKey {
match self {
WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()),
WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty_arc()),
}
}
@ -440,9 +440,7 @@ impl Worktree {
abs_path
.file_name()
.and_then(|f| f.to_str())
.map_or(RelPath::empty().into(), |f| {
RelPath::unix(f).unwrap().into()
}),
.map_or(RelPath::empty_arc(), |f| RelPath::unix(f).unwrap().into()),
abs_path.clone(),
PathStyle::local(),
),
@ -471,7 +469,7 @@ impl Worktree {
let share_private_files = false;
if let Some(metadata) = metadata {
let mut entry = Entry::new(
RelPath::empty().into(),
RelPath::empty_arc(),
&metadata,
ProjectEntryId::new(&next_entry_id),
snapshot.root_char_bag,
@ -529,8 +527,7 @@ impl Worktree {
cx.new(|cx: &mut Context<Self>| {
let mut snapshot = Snapshot::new(
WorktreeId::from_proto(worktree.id),
RelPath::from_proto(&worktree.root_name)
.unwrap_or_else(|_| RelPath::empty().into()),
RelPath::from_proto(&worktree.root_name).unwrap_or_else(|_| RelPath::empty_arc()),
Path::new(&worktree.abs_path).into(),
path_style,
);
@ -1552,7 +1549,7 @@ impl LocalWorktree {
}
}
lowest_ancestor.unwrap_or_else(|| RelPath::empty().into())
lowest_ancestor.unwrap_or_else(|| RelPath::empty_arc())
}
pub fn create_entry(
@ -2058,9 +2055,7 @@ impl LocalWorktree {
.as_path()
.file_name()
.and_then(|f| f.to_str())
.map_or(RelPath::empty().into(), |f| {
RelPath::unix(f).unwrap().into()
});
.map_or(RelPath::empty_arc(), |f| RelPath::unix(f).unwrap().into());
self.snapshot.update_abs_path(new_path, root_name);
self.restart_background_scanners(cx);
}
@ -3698,7 +3693,7 @@ impl<S: Summary> Summary for PathSummary<S> {
fn zero(cx: Self::Context<'_>) -> Self {
Self {
max_path: RelPath::empty().into(),
max_path: RelPath::empty_arc(),
item_summary: S::zero(cx),
}
}
@ -3955,7 +3950,7 @@ pub struct PathKey(pub Arc<RelPath>);
impl Default for PathKey {
fn default() -> Self {
Self(RelPath::empty().into())
Self(RelPath::empty_arc())
}
}