sidebar: Improve performance of rebuild_contents (#57717)

Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth 2026-05-27 12:31:57 +02:00 committed by GitHub
parent 75c17a6ee9
commit 6555ac3d04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 105 additions and 80 deletions

View file

@ -936,7 +936,11 @@ impl ActionLog {
let mut undo_buffers = Vec::new();
let mut futures = Vec::new();
for buffer in self.changed_buffers(cx).into_keys() {
for buffer in self
.changed_buffers(cx)
.map(|(buffer, _)| buffer)
.collect::<Vec<_>>()
{
let buffer_ranges = vec![Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
)];
@ -1023,17 +1027,19 @@ impl ActionLog {
}
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
pub fn changed_buffers(
&self,
cx: &App,
) -> impl Iterator<Item = (Entity<Buffer>, Entity<BufferDiff>)> {
self.tracked_buffers
.iter()
.filter(|(_, tracked)| tracked.has_edits(cx))
.map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone()))
.collect()
}
/// Returns the total number of lines added and removed across all unreviewed buffers.
pub fn diff_stats(&self, cx: &App) -> DiffStats {
DiffStats::all_files(&self.changed_buffers(cx), cx)
DiffStats::all_files(self.changed_buffers(cx), cx)
}
/// Iterate over buffers changed since last read or edited by the model
@ -1079,7 +1085,7 @@ impl DiffStats {
}
pub fn all_files(
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
changed_buffers: impl IntoIterator<Item = (Entity<Buffer>, Entity<BufferDiff>)>,
cx: &App,
) -> Self {
let mut total = DiffStats::default();
@ -3254,21 +3260,21 @@ mod tests {
child_log_1
.read(cx)
.changed_buffers(cx)
.into_keys()
.map(|(buffer, _)| buffer)
.collect()
});
let child_2_changed: Vec<_> = cx.read(|cx| {
child_log_2
.read(cx)
.changed_buffers(cx)
.into_keys()
.map(|(buffer, _)| buffer)
.collect()
});
let parent_changed: Vec<_> = cx.read(|cx| {
parent_log
.read(cx)
.changed_buffers(cx)
.into_keys()
.map(|(buffer, _)| buffer)
.collect()
});
@ -3494,7 +3500,6 @@ mod tests {
action_log
.read(cx)
.changed_buffers(cx)
.into_iter()
.map(|(buffer, diff)| {
let snapshot = buffer.read(cx).snapshot();
(

View file

@ -2608,7 +2608,8 @@ mod tests {
cx.run_until_parked();
let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
let changed =
action_log.read_with(cx, |log, cx| log.changed_buffers(cx).collect::<Vec<_>>());
assert!(
!changed.is_empty(),
"action_log.changed_buffers() should be non-empty after streaming edit,

View file

@ -1061,7 +1061,8 @@ mod tests {
cx.run_until_parked();
let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
let changed =
action_log.read_with(cx, |log, cx| log.changed_buffers(cx).collect::<Vec<_>>());
assert!(
!changed.is_empty(),
"action_log.changed_buffers() should be non-empty after streaming write, \
@ -1133,7 +1134,8 @@ mod tests {
);
// Reject all edits — this should delete the newly created file
let changed = action_log.read_with(cx, |log, cx| log.changed_buffers(cx));
let changed =
action_log.read_with(cx, |log, cx| log.changed_buffers(cx).collect::<Vec<_>>());
assert!(
!changed.is_empty(),
"action_log should track the created file as changed"

View file

@ -138,7 +138,7 @@ impl AgentDiffPane {
.changed_buffers(cx);
// Sort edited files alphabetically for consistency with Git diff view
let mut sorted_buffers: Vec<_> = changed_buffers.iter().collect();
let mut sorted_buffers: Vec<_> = changed_buffers.collect();
sorted_buffers.sort_by(|(buffer_a, _), (buffer_b, _)| {
let path_a = buffer_a.read(cx).file().map(|f| f.path().clone());
let path_b = buffer_b.read(cx).file().map(|f| f.path().clone());
@ -1535,7 +1535,7 @@ impl AgentDiff {
};
let action_log = thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let changed_buffers = action_log.read(cx).changed_buffers(cx).collect::<Vec<_>>();
let mut unaffected = self.reviewing_editors.clone();
@ -1769,12 +1769,13 @@ impl AgentDiff {
{
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
let mut keys = changed_buffers.keys();
keys.find(|k| *k == &curr_buffer);
let mut keys = changed_buffers.map(|(buffer, _)| buffer);
keys.find(|k| *k == curr_buffer);
let next_project_path = keys
.next()
.filter(|k| *k != &curr_buffer)
.filter(|k| *k != curr_buffer)
.and_then(|after| after.read(cx).project_path(cx));
drop(keys);
if let Some(path) = next_project_path {
let task = workspace.open_path(path, None, true, window, cx);

View file

@ -53,7 +53,7 @@ use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use std::{rc::Rc, time::Duration};
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme_settings::{AgentBufferFontSize, AgentUiFontSize};

View file

@ -2310,7 +2310,7 @@ impl ThreadView {
let thread = &self.thread;
let telemetry = ActionLogTelemetry::from(thread.read(cx));
let action_log = thread.read(cx).action_log().clone();
let has_changes = action_log.read(cx).changed_buffers(cx).len() > 0;
let has_changes = action_log.read(cx).changed_buffers(cx).next().is_some();
action_log
.update(cx, |action_log, cx| {
@ -2580,7 +2580,7 @@ impl ThreadView {
let thread = self.thread.read(cx);
let action_log = thread.action_log();
let telemetry = ActionLogTelemetry::from(thread);
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let changed_buffers = action_log.read(cx).changed_buffers(cx).collect::<Vec<_>>();
let plan = thread.plan();
let queue_is_empty = !self.has_queued_messages();
@ -2686,7 +2686,7 @@ impl ThreadView {
&self,
action_log: &Entity<ActionLog>,
telemetry: ActionLogTelemetry,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
changed_buffers: &[(Entity<Buffer>, Entity<BufferDiff>)],
pending_edits: bool,
cx: &Context<Self>,
) -> impl IntoElement {
@ -3433,7 +3433,7 @@ impl ThreadView {
fn render_edits_summary(
&self,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
changed_buffers: &[(Entity<Buffer>, Entity<BufferDiff>)],
expanded: bool,
pending_edits: bool,
cx: &Context<Self>,
@ -3478,7 +3478,7 @@ impl ThreadView {
),
)
} else {
let stats = DiffStats::all_files(changed_buffers, cx);
let stats = DiffStats::all_files(changed_buffers.iter().cloned(), cx);
let dot_divider = || {
Label::new("")
.size(LabelSize::XSmall)
@ -8391,7 +8391,7 @@ impl ThreadView {
.map(|thread| thread.read(cx).session_id().clone());
let action_log = thread.as_ref().map(|thread| thread.read(cx).action_log());
let changed_buffers = action_log
.map(|log| log.read(cx).changed_buffers(cx))
.map(|log| log.read(cx).changed_buffers(cx).collect::<Vec<_>>())
.unwrap_or_default();
let is_pending_tool_call = thread_view
@ -8404,7 +8404,7 @@ impl ThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let files_changed = changed_buffers.len();
let diff_stats = DiffStats::all_files(&changed_buffers, cx);
let diff_stats = DiffStats::all_files(changed_buffers, cx);
let is_running = matches!(
tool_call.status,

View file

@ -234,12 +234,6 @@ impl TextSystem {
Ok(self.advance(font_id, font_size, 'm')?.width)
}
// Consider removing this?
/// Returns the shaped layout width of an `em`.
pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.layout_width(font_id, font_size, 'm')
}
/// Returns the width of an `ch`.
///
/// Uses the width of the `0` character in the given font and size.
@ -699,6 +693,28 @@ impl WindowTextSystem {
layout
}
/// Returns the shaped layout width of for the given character, in the given font and size.
pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
let mut buffer = [0; 4];
let buffer: &_ = ch.encode_utf8(&mut buffer);
self.line_layout_cache
.layout_line(
buffer,
font_size,
&[FontRun {
len: buffer.len(),
font_id,
}],
None,
)
.width
}
/// Returns the shaped layout width of an `em`.
pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.layout_width(font_id, font_size, 'm')
}
/// Probe the line layout cache using a caller-provided content hash, without allocating.
///
/// Returns `Some(layout)` if the layout is already cached in either the current frame

View file

@ -307,7 +307,7 @@ enum ListEntry {
is_active: bool,
has_threads: bool,
},
Thread(ThreadEntry),
Thread(Arc<ThreadEntry>),
Terminal(TerminalEntry),
}
@ -403,7 +403,7 @@ impl ListEntry {
impl From<ThreadEntry> for ListEntry {
fn from(thread: ThreadEntry) -> Self {
ListEntry::Thread(thread)
ListEntry::Thread(Arc::new(thread))
}
}
@ -479,24 +479,22 @@ fn linked_worktree_path_lists_for_workspaces(
workspaces: &[Entity<Workspace>],
cx: &App,
) -> Vec<PathList> {
let mut linked_worktree_paths = HashSet::new();
let mut linked_worktree_paths = Vec::new();
for workspace in workspaces {
if workspace.read(cx).visible_worktrees(cx).count() != 1 {
continue;
}
for snapshot in root_repository_snapshots(workspace, cx) {
for linked_worktree in snapshot.linked_worktrees() {
linked_worktree_paths.insert(linked_worktree.path.clone());
}
linked_worktree_paths.extend(
snapshot.linked_worktrees().iter().map(|linked_worktree| {
PathList::new(std::slice::from_ref(&linked_worktree.path))
}),
);
}
}
let mut linked_worktree_paths = linked_worktree_paths.into_iter().collect::<Vec<_>>();
linked_worktree_paths.sort();
linked_worktree_paths.sort_by(|a, b| a.paths()[0].cmp(&b.paths()[0]));
linked_worktree_paths
.into_iter()
.map(|path| PathList::new(std::slice::from_ref(&path)))
.collect()
}
fn workspace_has_terminal_metadata_except(
@ -1439,12 +1437,11 @@ impl Sidebar {
.is_some_and(|active| group_workspaces.contains(active));
// Collect live thread infos from all workspaces in this group.
let live_infos: Vec<_> = group_workspaces
let live_infos = group_workspaces
.iter()
.flat_map(|ws| all_thread_infos_for_workspace(ws, cx))
.collect();
.flat_map(|ws| all_thread_infos_for_workspace(ws, cx));
let mut threads: Vec<ThreadEntry> = Vec::new();
let mut threads: Vec<Arc<ThreadEntry>> = Vec::new();
let mut has_running_threads = false;
let mut waiting_thread_count: usize = 0;
let group_host = group_key.host();
@ -1461,12 +1458,12 @@ impl Sidebar {
let thread_store = ThreadMetadataStore::global(cx);
let make_thread_entry =
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> ThreadEntry {
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> Arc<ThreadEntry> {
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
let worktrees =
worktree_info_from_thread_paths(&row.worktree_paths, &branch_by_path);
let is_draft = row.is_draft();
ThreadEntry {
Arc::new(ThreadEntry {
metadata: row,
icon,
icon_from_external_svg,
@ -1479,7 +1476,7 @@ impl Sidebar {
highlight_positions: Vec::new(),
worktrees,
diff_stats: DiffStats::default(),
}
})
};
// Main code path: one query per group via main_worktree_paths.
@ -1574,7 +1571,7 @@ impl Sidebar {
if !thread.is_draft {
continue;
}
thread.metadata.title = draft_display_label_for_thread_metadata(
Arc::make_mut(thread).metadata.title = draft_display_label_for_thread_metadata(
&thread.metadata,
&thread.workspace,
cx,
@ -1584,26 +1581,26 @@ impl Sidebar {
// Build a lookup from live_infos and compute running/waiting
// counts in a single pass.
let mut live_info_by_session: HashMap<&acp::SessionId, &ActiveThreadInfo> =
let mut live_info_by_session: HashMap<acp::SessionId, ActiveThreadInfo> =
HashMap::new();
for info in &live_infos {
live_info_by_session.insert(&info.session_id, info);
for info in live_infos {
if info.status == AgentThreadStatus::Running {
has_running_threads = true;
}
if info.status == AgentThreadStatus::WaitingForConfirmation {
waiting_thread_count += 1;
}
live_info_by_session.insert(info.session_id.clone(), info);
}
// Merge live info into threads and update notification state
// in a single pass.
for thread in &mut threads {
if let Some(session_id) = thread.metadata.session_id.clone() {
if let Some(&info) = live_info_by_session.get(&session_id) {
if let Some(info) = live_info_by_session.get(&session_id) {
let status = info.status;
let thread_id = thread.metadata.thread_id;
thread.apply_active_info(info);
Arc::make_mut(thread).apply_active_info(info);
new_live_statuses.insert(session_id, (status, thread_id));
}
}
@ -1637,7 +1634,7 @@ impl Sidebar {
b_time.cmp(&a_time)
});
} else {
for info in &live_infos {
for info in live_infos {
if info.status == AgentThreadStatus::Running {
has_running_threads = true;
}
@ -1708,20 +1705,23 @@ impl Sidebar {
fuzzy_match_positions(&query, &label).unwrap_or_default();
let workspace_matched = !workspace_highlight_positions.is_empty();
let mut matched_threads: Vec<ThreadEntry> = Vec::new();
let mut matched_threads: Vec<Arc<ThreadEntry>> = Vec::new();
for mut thread in threads {
let title = thread.metadata.display_title();
if let Some(positions) = fuzzy_match_positions(&query, title.as_ref()) {
thread.highlight_positions = positions;
}
let mut worktree_matched = false;
for worktree in &mut thread.worktrees {
let Some(name) = worktree.worktree_name.as_ref() else {
continue;
};
if let Some(positions) = fuzzy_match_positions(&query, name) {
worktree.highlight_positions = positions;
worktree_matched = true;
{
let thread = Arc::make_mut(&mut thread);
let title = thread.metadata.display_title();
if let Some(positions) = fuzzy_match_positions(&query, title.as_ref()) {
thread.highlight_positions = positions;
}
for worktree in &mut thread.worktrees {
let Some(name) = worktree.worktree_name.as_ref() else {
continue;
};
if let Some(positions) = fuzzy_match_positions(&query, name) {
worktree.highlight_positions = positions;
worktree_matched = true;
}
}
}
if workspace_matched
@ -4723,7 +4723,7 @@ impl Sidebar {
.as_ref()
.map(|workspace| PathList::new(&workspace.read(cx).root_paths(cx)))
});
let thread_entry_workspace = thread_entry.map(|thread| thread.workspace);
let thread_entry_workspace = thread_entry.map(|thread| thread.workspace.clone());
if let (
Some(metadata),
@ -5229,7 +5229,7 @@ impl Sidebar {
fn push_entries_by_display_time(
entries: &mut Vec<ListEntry>,
terminals: Vec<TerminalEntry>,
threads: Vec<ThreadEntry>,
threads: Vec<Arc<ThreadEntry>>,
current_session_ids: &mut HashSet<acp::SessionId>,
current_thread_ids: &mut HashSet<agent_ui::ThreadId>,
) {

View file

@ -1089,7 +1089,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
is_active: true,
has_threads: true,
},
ListEntry::Thread(ThreadEntry {
ListEntry::Thread(Arc::new(ThreadEntry {
metadata: ThreadMetadata {
thread_id: ThreadId::new(),
session_id: Some(acp::SessionId::new(Arc::from("t-1"))),
@ -1114,9 +1114,9 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
highlight_positions: Vec::new(),
worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
})),
// Active thread with Running status
ListEntry::Thread(ThreadEntry {
ListEntry::Thread(Arc::new(ThreadEntry {
metadata: ThreadMetadata {
thread_id: ThreadId::new(),
session_id: Some(acp::SessionId::new(Arc::from("t-2"))),
@ -1141,9 +1141,9 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
highlight_positions: Vec::new(),
worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
})),
// Active thread with Error status
ListEntry::Thread(ThreadEntry {
ListEntry::Thread(Arc::new(ThreadEntry {
metadata: ThreadMetadata {
thread_id: ThreadId::new(),
session_id: Some(acp::SessionId::new(Arc::from("t-3"))),
@ -1168,10 +1168,10 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
highlight_positions: Vec::new(),
worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
})),
// Thread with WaitingForConfirmation status, not active
// remote_connection: None,
ListEntry::Thread(ThreadEntry {
ListEntry::Thread(Arc::new(ThreadEntry {
metadata: ThreadMetadata {
thread_id: ThreadId::new(),
session_id: Some(acp::SessionId::new(Arc::from("t-4"))),
@ -1196,10 +1196,10 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
highlight_positions: Vec::new(),
worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
})),
// Background thread that completed (should show notification)
// remote_connection: None,
ListEntry::Thread(ThreadEntry {
ListEntry::Thread(Arc::new(ThreadEntry {
metadata: ThreadMetadata {
thread_id: notified_thread_id,
session_id: Some(acp::SessionId::new(Arc::from("t-5"))),
@ -1224,7 +1224,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
highlight_positions: Vec::new(),
worktrees: Vec::new(),
diff_stats: DiffStats::default(),
}),
})),
// Collapsed project header
ListEntry::ProjectHeader {
key: ProjectGroupKey::new(None, collapsed_path.clone()),