diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 0471ae45dd8..99cc0a2d79b 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -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::>() + { 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> { + pub fn changed_buffers( + &self, + cx: &App, + ) -> impl Iterator, Entity)> { 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>, + changed_buffers: impl IntoIterator, Entity)>, 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(); ( diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 82c6463dd11..2801c8878d1 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -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::>()); assert!( !changed.is_empty(), "action_log.changed_buffers() should be non-empty after streaming edit, diff --git a/crates/agent/src/tools/write_file_tool.rs b/crates/agent/src/tools/write_file_tool.rs index 89942c316ce..735a9d23a91 100644 --- a/crates/agent/src/tools/write_file_tool.rs +++ b/crates/agent/src/tools/write_file_tool.rs @@ -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::>()); 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::>()); assert!( !changed.is_empty(), "action_log should track the created file as changed" diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 95a9191ef85..9a820db7b20 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -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::>(); 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); diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index fc266cbed42..c1646179483 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -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}; diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index c4416f06d7a..857ef88dbc3 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -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::>(); let plan = thread.plan(); let queue_is_empty = !self.has_queued_messages(); @@ -2686,7 +2686,7 @@ impl ThreadView { &self, action_log: &Entity, telemetry: ActionLogTelemetry, - changed_buffers: &BTreeMap, Entity>, + changed_buffers: &[(Entity, Entity)], pending_edits: bool, cx: &Context, ) -> impl IntoElement { @@ -3433,7 +3433,7 @@ impl ThreadView { fn render_edits_summary( &self, - changed_buffers: &BTreeMap, Entity>, + changed_buffers: &[(Entity, Entity)], expanded: bool, pending_edits: bool, cx: &Context, @@ -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::>()) .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, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index bebe180b262..043d37f679b 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -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 diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 948650d0959..81d65e63366 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -307,7 +307,7 @@ enum ListEntry { is_active: bool, has_threads: bool, }, - Thread(ThreadEntry), + Thread(Arc), Terminal(TerminalEntry), } @@ -403,7 +403,7 @@ impl ListEntry { impl From 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], cx: &App, ) -> Vec { - 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::>(); - 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 = Vec::new(); + let mut threads: Vec> = 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 { 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 = 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 = Vec::new(); + let mut matched_threads: Vec> = 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, terminals: Vec, - threads: Vec, + threads: Vec>, current_session_ids: &mut HashSet, current_thread_ids: &mut HashSet, ) { diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 97ddea152d1..40cfc62fa39 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -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()),