sidebar: Show branch name after worktree name (#53900)
<img width="297" height="505" alt="Screenshot 2026-04-14 at 3 20 23 PM" src="https://github.com/user-attachments/assets/90366ccd-86db-497e-9c81-af94b4144ebe" /> Display the git branch name alongside the worktree name in the sidebar's thread list (or without the worktree name, if it's a main worktree). Release Notes: - Added branch name display next to worktree names in the agent sidebar thread list.
This commit is contained in:
1 parent
4a41cc4bf0
commit
21f09886d0
6 files changed
+723
-198
No files matched your search
@@ -21,10 +21,10 @@ use db::{
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{AppContext as _, Entity, Global, Subscription, Task};
|
||||
use project::AgentId;
|
||||
pub use project::WorktreePaths;
|
||||
use project::{AgentId, linked_worktree_short_name};
|
||||
use remote::{RemoteConnectionOptions, same_remote_connection_identity};
|
||||
use ui::{App, Context, SharedString};
|
||||
use ui::{App, Context, SharedString, ThreadItemWorktreeInfo, WorktreeKind};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{PathList, SerializedWorkspaceLocation, WorkspaceDb};
|
||||
|
||||
@@ -314,6 +314,73 @@ impl ThreadMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives worktree display info from a thread's stored path list.
|
||||
///
|
||||
/// For each path in the thread's `folder_paths`, produces a
|
||||
/// [`ThreadItemWorktreeInfo`] with a short display name, full path, and whether
|
||||
/// the worktree is the main checkout or a linked git worktree. When
|
||||
/// multiple main paths exist and a linked worktree's short name alone
|
||||
/// wouldn't identify which main project it belongs to, the main project
|
||||
/// name is prefixed for disambiguation (e.g. `project:feature`).
|
||||
pub fn worktree_info_from_thread_paths<S: std::hash::BuildHasher>(
|
||||
worktree_paths: &WorktreePaths,
|
||||
branch_names: &std::collections::HashMap<PathBuf, SharedString, S>,
|
||||
) -> Vec<ThreadItemWorktreeInfo> {
|
||||
let mut infos: Vec<ThreadItemWorktreeInfo> = Vec::new();
|
||||
let mut linked_short_names: Vec<(SharedString, SharedString)> = Vec::new();
|
||||
let mut unique_main_count = HashSet::default();
|
||||
|
||||
for (main_path, folder_path) in worktree_paths.ordered_pairs() {
|
||||
unique_main_count.insert(main_path.clone());
|
||||
let is_linked = main_path != folder_path;
|
||||
|
||||
if is_linked {
|
||||
let short_name = linked_worktree_short_name(main_path, folder_path).unwrap_or_default();
|
||||
let project_name = main_path
|
||||
.file_name()
|
||||
.map(|n| SharedString::from(n.to_string_lossy().to_string()))
|
||||
.unwrap_or_default();
|
||||
linked_short_names.push((short_name.clone(), project_name));
|
||||
infos.push(ThreadItemWorktreeInfo {
|
||||
name: short_name,
|
||||
full_path: SharedString::from(folder_path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: branch_names.get(folder_path).cloned(),
|
||||
});
|
||||
} else {
|
||||
let Some(name) = folder_path.file_name() else {
|
||||
continue;
|
||||
};
|
||||
infos.push(ThreadItemWorktreeInfo {
|
||||
name: SharedString::from(name.to_string_lossy().to_string()),
|
||||
full_path: SharedString::from(folder_path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Main,
|
||||
branch_name: branch_names.get(folder_path).cloned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When the group has multiple main worktree paths and the thread's
|
||||
// folder paths don't all share the same short name, prefix each
|
||||
// linked worktree chip with its main project name so the user knows
|
||||
// which project it belongs to.
|
||||
let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
|
||||
|
||||
if unique_main_count.len() > 1 && !all_same_name {
|
||||
for (info, (_short_name, project_name)) in infos
|
||||
.iter_mut()
|
||||
.filter(|i| i.kind == WorktreeKind::Linked)
|
||||
.zip(linked_short_names.iter())
|
||||
{
|
||||
info.name = SharedString::from(format!("{}:{}", project_name, info.name));
|
||||
}
|
||||
}
|
||||
|
||||
infos
|
||||
}
|
||||
|
||||
impl From<&ThreadMetadata> for acp_thread::AgentSessionInfo {
|
||||
fn from(meta: &ThreadMetadata) -> Self {
|
||||
let session_id = meta
|
||||
@@ -938,6 +1005,14 @@ impl ThreadMetadataStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_archived_branch_names(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> Task<anyhow::Result<HashMap<ThreadId, HashMap<PathBuf, String>>>> {
|
||||
let db = self.db.clone();
|
||||
cx.background_spawn(async move { db.get_all_archived_branch_names() })
|
||||
}
|
||||
|
||||
fn update_archived(&mut self, thread_id: ThreadId, archived: bool, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.threads.get(&thread_id) {
|
||||
self.save_internal(ThreadMetadata {
|
||||
@@ -1094,10 +1169,10 @@ impl ThreadMetadataStore {
|
||||
)
|
||||
} else {
|
||||
let project = thread_ref.project().read(cx);
|
||||
(
|
||||
project.worktree_paths(cx),
|
||||
project.remote_connection_options(cx),
|
||||
)
|
||||
let worktree_paths = project.worktree_paths(cx);
|
||||
let remote_connection = project.remote_connection_options(cx);
|
||||
|
||||
(worktree_paths, remote_connection)
|
||||
};
|
||||
|
||||
// Threads without a folder path (e.g. started in an empty
|
||||
@@ -1406,6 +1481,27 @@ impl ThreadMetadataDb {
|
||||
)?(archived_worktree_id)
|
||||
.map(|count| count.unwrap_or(0) > 0)
|
||||
}
|
||||
|
||||
pub fn get_all_archived_branch_names(
|
||||
&self,
|
||||
) -> anyhow::Result<HashMap<ThreadId, HashMap<PathBuf, String>>> {
|
||||
let rows = self.select::<(ThreadId, String, String)>(
|
||||
"SELECT t.thread_id, a.worktree_path, a.branch_name \
|
||||
FROM thread_archived_worktrees t \
|
||||
JOIN archived_git_worktrees a ON a.id = t.archived_worktree_id \
|
||||
WHERE a.branch_name IS NOT NULL \
|
||||
ORDER BY a.id ASC",
|
||||
)?()?;
|
||||
|
||||
let mut result: HashMap<ThreadId, HashMap<PathBuf, String>> = HashMap::default();
|
||||
for (thread_id, worktree_path, branch_name) in rows {
|
||||
result
|
||||
.entry(thread_id)
|
||||
.or_default()
|
||||
.insert(PathBuf::from(worktree_path), branch_name);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for ThreadMetadata {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_connection_store::AgentConnectionStore;
|
||||
|
||||
use crate::thread_metadata_store::{ThreadId, ThreadMetadata, ThreadMetadataStore};
|
||||
use crate::thread_metadata_store::{
|
||||
ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths,
|
||||
};
|
||||
use crate::{Agent, DEFAULT_THREAD_TITLE, RemoveSelectedThread};
|
||||
|
||||
use agent::ThreadStore;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
@@ -133,6 +137,9 @@ pub struct ThreadsArchiveView {
|
||||
agent_connection_store: WeakEntity<AgentConnectionStore>,
|
||||
agent_server_store: WeakEntity<AgentServerStore>,
|
||||
restoring: HashSet<ThreadId>,
|
||||
archived_thread_ids: HashSet<ThreadId>,
|
||||
archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>,
|
||||
_load_branch_names_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ThreadsArchiveView {
|
||||
@@ -175,6 +182,7 @@ impl ThreadsArchiveView {
|
||||
&ThreadMetadataStore::global(cx),
|
||||
|this: &mut Self, _, cx| {
|
||||
this.update_items(cx);
|
||||
this.reload_branch_names_if_threads_changed(cx);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -202,9 +210,13 @@ impl ThreadsArchiveView {
|
||||
agent_connection_store,
|
||||
agent_server_store,
|
||||
restoring: HashSet::default(),
|
||||
archived_thread_ids: HashSet::default(),
|
||||
archived_branch_names: HashMap::default(),
|
||||
_load_branch_names_task: Task::ready(()),
|
||||
};
|
||||
|
||||
this.update_items(cx);
|
||||
this.reload_branch_names_if_threads_changed(cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -331,6 +343,37 @@ impl ThreadsArchiveView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn reload_branch_names_if_threads_changed(&mut self, cx: &mut Context<Self>) {
|
||||
let current_ids: HashSet<ThreadId> = self
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ArchiveListItem::Entry { thread, .. } => Some(thread.thread_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if current_ids != self.archived_thread_ids {
|
||||
self.archived_thread_ids = current_ids;
|
||||
self.load_archived_branch_names(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_archived_branch_names(&mut self, cx: &mut Context<Self>) {
|
||||
let task = ThreadMetadataStore::global(cx)
|
||||
.read(cx)
|
||||
.get_all_archived_branch_names(cx);
|
||||
self._load_branch_names_task = cx.spawn(async move |this, cx| {
|
||||
if let Some(branch_names) = task.await.log_err() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.archived_branch_names = branch_names;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.filter_editor.update(cx, |editor, cx| {
|
||||
editor.set_text("", window, cx);
|
||||
@@ -537,6 +580,20 @@ impl ThreadsArchiveView {
|
||||
|
||||
let is_restoring = self.restoring.contains(&thread.thread_id);
|
||||
|
||||
let branch_names_for_thread: HashMap<PathBuf, SharedString> = self
|
||||
.archived_branch_names
|
||||
.get(&thread.thread_id)
|
||||
.map(|map| {
|
||||
map.iter()
|
||||
.map(|(k, v)| (k.clone(), SharedString::from(v.clone())))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let worktrees = worktree_info_from_thread_paths(
|
||||
&thread.worktree_paths,
|
||||
&branch_names_for_thread,
|
||||
);
|
||||
|
||||
let base = ThreadItem::new(id, thread.display_title())
|
||||
.icon(icon)
|
||||
.when_some(icon_from_external_svg, |this, svg| {
|
||||
@@ -544,7 +601,7 @@ impl ThreadsArchiveView {
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.highlight_positions(highlight_positions.clone())
|
||||
.project_paths(thread.folder_paths().paths_owned())
|
||||
.worktrees(worktrees)
|
||||
.focused(is_focused)
|
||||
.hovered(is_hovered)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
|
||||
@@ -4,7 +4,9 @@ use acp_thread::ThreadStatus;
|
||||
use action_log::DiffStats;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_settings::AgentSettings;
|
||||
use agent_ui::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore, WorktreePaths};
|
||||
use agent_ui::thread_metadata_store::{
|
||||
ThreadMetadata, ThreadMetadataStore, WorktreePaths, worktree_info_from_thread_paths,
|
||||
};
|
||||
use agent_ui::thread_worktree_archive;
|
||||
use agent_ui::threads_archive_view::{
|
||||
ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
|
||||
@@ -23,9 +25,7 @@ use gpui::{
|
||||
use menu::{
|
||||
Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
|
||||
};
|
||||
use project::{
|
||||
AgentId, AgentRegistryStore, Event as ProjectEvent, WorktreeId, linked_worktree_short_name,
|
||||
};
|
||||
use project::{AgentId, AgentRegistryStore, Event as ProjectEvent, WorktreeId};
|
||||
use recent_projects::sidebar_recent_projects::SidebarRecentProjects;
|
||||
use remote::{RemoteConnectionOptions, same_remote_connection_identity};
|
||||
use ui::utils::platform_title_bar_height;
|
||||
@@ -36,6 +36,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, GradientFade, HighlightedLabel,
|
||||
@@ -181,14 +182,6 @@ impl ThreadEntryWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WorktreeInfo {
|
||||
name: SharedString,
|
||||
full_path: SharedString,
|
||||
highlight_positions: Vec<usize>,
|
||||
kind: ui::WorktreeKind,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ThreadEntry {
|
||||
metadata: ThreadMetadata,
|
||||
@@ -201,7 +194,7 @@ struct ThreadEntry {
|
||||
is_title_generating: bool,
|
||||
is_draft: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
worktrees: Vec<WorktreeInfo>,
|
||||
worktrees: Vec<ThreadItemWorktreeInfo>,
|
||||
diff_stats: DiffStats,
|
||||
}
|
||||
|
||||
@@ -336,69 +329,6 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
|
||||
PathList::new(&workspace.read(cx).root_paths(cx))
|
||||
}
|
||||
|
||||
/// Derives worktree display info from a thread's stored path list.
|
||||
///
|
||||
/// For each path in the thread's `folder_paths`, produces a
|
||||
/// [`WorktreeInfo`] with a short display name, full path, and whether
|
||||
/// the worktree is the main checkout or a linked git worktree. When
|
||||
/// multiple main paths exist and a linked worktree's short name alone
|
||||
/// wouldn't identify which main project it belongs to, the main project
|
||||
/// name is prefixed for disambiguation (e.g. `project:feature`).
|
||||
///
|
||||
fn worktree_info_from_thread_paths(worktree_paths: &WorktreePaths) -> Vec<WorktreeInfo> {
|
||||
let mut infos: Vec<WorktreeInfo> = Vec::new();
|
||||
let mut linked_short_names: Vec<(SharedString, SharedString)> = Vec::new();
|
||||
let mut unique_main_count = HashSet::new();
|
||||
|
||||
for (main_path, folder_path) in worktree_paths.ordered_pairs() {
|
||||
unique_main_count.insert(main_path.clone());
|
||||
let is_linked = main_path != folder_path;
|
||||
|
||||
if is_linked {
|
||||
let short_name = linked_worktree_short_name(main_path, folder_path).unwrap_or_default();
|
||||
let project_name = main_path
|
||||
.file_name()
|
||||
.map(|n| SharedString::from(n.to_string_lossy().to_string()))
|
||||
.unwrap_or_default();
|
||||
linked_short_names.push((short_name.clone(), project_name));
|
||||
infos.push(WorktreeInfo {
|
||||
name: short_name,
|
||||
full_path: SharedString::from(folder_path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Linked,
|
||||
});
|
||||
} else {
|
||||
let Some(name) = folder_path.file_name() else {
|
||||
continue;
|
||||
};
|
||||
infos.push(WorktreeInfo {
|
||||
name: SharedString::from(name.to_string_lossy().to_string()),
|
||||
full_path: SharedString::from(folder_path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Main,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When the group has multiple main worktree paths and the thread's
|
||||
// folder paths don't all share the same short name, prefix each
|
||||
// linked worktree chip with its main project name so the user knows
|
||||
// which project it belongs to.
|
||||
let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);
|
||||
|
||||
if unique_main_count.len() > 1 && !all_same_name {
|
||||
for (info, (_short_name, project_name)) in infos
|
||||
.iter_mut()
|
||||
.filter(|i| i.kind == ui::WorktreeKind::Linked)
|
||||
.zip(linked_short_names.iter())
|
||||
{
|
||||
info.name = SharedString::from(format!("{}:{}", project_name, info.name));
|
||||
}
|
||||
}
|
||||
|
||||
infos
|
||||
}
|
||||
|
||||
/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
|
||||
/// an SSH connection. Suitable for passing to
|
||||
/// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`
|
||||
@@ -636,7 +566,8 @@ impl Sidebar {
|
||||
event,
|
||||
project::git_store::GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
project::git_store::RepositoryEvent::GitWorktreeListChanged,
|
||||
project::git_store::RepositoryEvent::GitWorktreeListChanged
|
||||
| project::git_store::RepositoryEvent::HeadChanged,
|
||||
_,
|
||||
)
|
||||
) {
|
||||
@@ -1080,6 +1011,28 @@ impl Sidebar {
|
||||
let path_detail_map: HashMap<PathBuf, usize> =
|
||||
all_paths.into_iter().zip(path_details).collect();
|
||||
|
||||
let mut branch_by_path: HashMap<PathBuf, SharedString> = HashMap::new();
|
||||
for ws in &workspaces {
|
||||
let project = ws.read(cx).project().read(cx);
|
||||
for repo in project.repositories(cx).values() {
|
||||
let snapshot = repo.read(cx).snapshot();
|
||||
if let Some(branch) = &snapshot.branch {
|
||||
branch_by_path.insert(
|
||||
snapshot.work_directory_abs_path.to_path_buf(),
|
||||
SharedString::from(Arc::<str>::from(branch.name())),
|
||||
);
|
||||
}
|
||||
for linked_wt in snapshot.linked_worktrees() {
|
||||
if let Some(branch) = linked_wt.branch_name() {
|
||||
branch_by_path.insert(
|
||||
linked_wt.path.clone(),
|
||||
SharedString::from(Arc::<str>::from(branch)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for group in &groups {
|
||||
let group_key = &group.key;
|
||||
let group_workspaces = &group.workspaces;
|
||||
@@ -1136,7 +1089,8 @@ impl Sidebar {
|
||||
let make_thread_entry =
|
||||
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> ThreadEntry {
|
||||
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
|
||||
let worktrees = worktree_info_from_thread_paths(&row.worktree_paths);
|
||||
let worktrees =
|
||||
worktree_info_from_thread_paths(&row.worktree_paths, &branch_by_path);
|
||||
let is_draft = row.is_draft();
|
||||
ThreadEntry {
|
||||
metadata: row,
|
||||
@@ -3546,11 +3500,10 @@ impl Sidebar {
|
||||
worktrees: thread
|
||||
.worktrees
|
||||
.iter()
|
||||
.map(|wt| ThreadItemWorktreeInfo {
|
||||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: wt.kind,
|
||||
.cloned()
|
||||
.map(|mut wt| {
|
||||
wt.highlight_positions = Vec::new();
|
||||
wt
|
||||
})
|
||||
.collect(),
|
||||
diff_stats: thread.diff_stats,
|
||||
@@ -3815,18 +3768,7 @@ impl Sidebar {
|
||||
.when_some(thread.icon_from_external_svg.clone(), |this, svg| {
|
||||
this.custom_icon_from_external_svg(svg)
|
||||
})
|
||||
.worktrees(
|
||||
thread
|
||||
.worktrees
|
||||
.iter()
|
||||
.map(|wt| ThreadItemWorktreeInfo {
|
||||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: wt.highlight_positions.clone(),
|
||||
kind: wt.kind,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.worktrees(thread.worktrees.clone())
|
||||
.timestamp(timestamp)
|
||||
.highlight_positions(thread.highlight_positions.to_vec())
|
||||
.title_generating(thread.is_title_generating)
|
||||
|
||||
@@ -356,7 +356,7 @@ fn request_test_tool_authorization(
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String {
|
||||
fn format_linked_worktree_chips(worktrees: &[ThreadItemWorktreeInfo]) -> String {
|
||||
let mut seen = Vec::new();
|
||||
let mut chips = Vec::new();
|
||||
for wt in worktrees {
|
||||
@@ -10888,3 +10888,57 @@ async fn test_archive_mixed_workspace_closes_only_archived_worktree_items(cx: &m
|
||||
"feature-b file should have been closed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_worktree_info_branch_names_for_main_worktrees() {
|
||||
let folder_paths = PathList::new(&[PathBuf::from("/projects/myapp")]);
|
||||
let worktree_paths = WorktreePaths::from_folder_paths(&folder_paths);
|
||||
|
||||
let branch_by_path: HashMap<PathBuf, SharedString> =
|
||||
[(PathBuf::from("/projects/myapp"), "feature-x".into())]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let infos = worktree_info_from_thread_paths(&worktree_paths, &branch_by_path);
|
||||
assert_eq!(infos.len(), 1);
|
||||
assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
|
||||
assert_eq!(infos[0].branch_name, Some(SharedString::from("feature-x")));
|
||||
assert_eq!(infos[0].name, SharedString::from("myapp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_worktree_info_branch_names_for_linked_worktrees() {
|
||||
let main_paths = PathList::new(&[PathBuf::from("/projects/myapp")]);
|
||||
let folder_paths = PathList::new(&[PathBuf::from("/projects/myapp-feature")]);
|
||||
let worktree_paths =
|
||||
WorktreePaths::from_path_lists(main_paths, folder_paths).expect("same length");
|
||||
|
||||
let branch_by_path: HashMap<PathBuf, SharedString> = [(
|
||||
PathBuf::from("/projects/myapp-feature"),
|
||||
"feature-branch".into(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let infos = worktree_info_from_thread_paths(&worktree_paths, &branch_by_path);
|
||||
assert_eq!(infos.len(), 1);
|
||||
assert_eq!(infos[0].kind, ui::WorktreeKind::Linked);
|
||||
assert_eq!(
|
||||
infos[0].branch_name,
|
||||
Some(SharedString::from("feature-branch"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_worktree_info_missing_branch_returns_none() {
|
||||
let folder_paths = PathList::new(&[PathBuf::from("/projects/myapp")]);
|
||||
let worktree_paths = WorktreePaths::from_folder_paths(&folder_paths);
|
||||
|
||||
let branch_by_path: HashMap<PathBuf, SharedString> = HashMap::new();
|
||||
|
||||
let infos = worktree_info_from_thread_paths(&worktree_paths, &branch_by_path);
|
||||
assert_eq!(infos.len(), 1);
|
||||
assert_eq!(infos[0].kind, ui::WorktreeKind::Main);
|
||||
assert_eq!(infos[0].branch_name, None);
|
||||
assert_eq!(infos[0].name, SharedString::from("myapp"));
|
||||
}
|
||||
@@ -29,6 +29,7 @@ pub struct ThreadItemWorktreeInfo {
|
||||
pub full_path: SharedString,
|
||||
pub highlight_positions: Vec<usize>,
|
||||
pub kind: WorktreeKind,
|
||||
pub branch_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
@@ -374,13 +375,119 @@ impl RenderOnce for ThreadItem {
|
||||
|
||||
let has_project_name = self.project_name.is_some();
|
||||
let has_project_paths = project_paths.is_some();
|
||||
let has_worktree = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.any(|wt| wt.kind == WorktreeKind::Linked);
|
||||
let has_timestamp = !self.timestamp.is_empty();
|
||||
let timestamp = self.timestamp;
|
||||
|
||||
let visible_worktree_count = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.filter(|wt| !(wt.kind == WorktreeKind::Main && wt.branch_name.is_none()))
|
||||
.count();
|
||||
|
||||
let worktree_tooltip_title = match (self.is_remote, visible_worktree_count > 1) {
|
||||
(true, true) => "Thread Running in Remote Git Worktrees",
|
||||
(true, false) => "Thread Running in a Remote Git Worktree",
|
||||
(false, true) => "Thread Running in Local Git Worktrees",
|
||||
(false, false) => "Thread Running in a Local Git Worktree",
|
||||
};
|
||||
|
||||
let mut worktree_labels: Vec<AnyElement> = Vec::new();
|
||||
|
||||
let slash_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.4));
|
||||
|
||||
for wt in self.worktrees {
|
||||
match (wt.kind, wt.branch_name) {
|
||||
(WorktreeKind::Main, None) => continue,
|
||||
(WorktreeKind::Main, Some(branch)) => {
|
||||
let chip_index = worktree_labels.len();
|
||||
let tooltip_title = worktree_tooltip_title;
|
||||
let full_path = wt.full_path.clone();
|
||||
|
||||
worktree_labels.push(
|
||||
h_flex()
|
||||
.id(format!("{}-worktree-{chip_index}", self.id.clone()))
|
||||
.min_w_0()
|
||||
.when(visible_worktree_count > 1, |this| {
|
||||
this.child(
|
||||
Label::new(wt.name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new("/")
|
||||
.size(LabelSize::Small)
|
||||
.color(slash_color)
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(branch)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(tooltip_title, None, full_path.clone(), cx)
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
(WorktreeKind::Linked, branch) => {
|
||||
let chip_index = worktree_labels.len();
|
||||
let tooltip_title = worktree_tooltip_title;
|
||||
let full_path = wt.full_path.clone();
|
||||
|
||||
let label = if wt.highlight_positions.is_empty() {
|
||||
Label::new(wt.name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
} else {
|
||||
HighlightedLabel::new(wt.name, wt.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
worktree_labels.push(
|
||||
h_flex()
|
||||
.id(format!("{}-worktree-{chip_index}", self.id.clone()))
|
||||
.min_w_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(IconName::GitWorktree)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(label)
|
||||
.when_some(branch, |this, branch| {
|
||||
this.child(
|
||||
Label::new("/")
|
||||
.size(LabelSize::Small)
|
||||
.color(slash_color)
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(
|
||||
Label::new(branch)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
)
|
||||
})
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(tooltip_title, None, full_path.clone(), cx)
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_worktree = !worktree_labels.is_empty();
|
||||
|
||||
v_flex()
|
||||
.id(self.id.clone())
|
||||
.cursor_pointer()
|
||||
@@ -441,102 +548,41 @@ impl RenderOnce for ThreadItem {
|
||||
|| has_diff_stats
|
||||
|| has_timestamp,
|
||||
|this| {
|
||||
// Collect all full paths for the shared tooltip.
|
||||
let worktree_tooltip: SharedString = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.map(|wt| wt.full_path.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
.into();
|
||||
|
||||
let worktree_tooltip_title = match (self.is_remote, self.worktrees.len() > 1) {
|
||||
(true, true) => "Thread Running in Remote Git Worktrees",
|
||||
(true, false) => "Thread Running in a Remote Git Worktree",
|
||||
(false, true) => "Thread Running in Local Git Worktrees",
|
||||
(false, false) => "Thread Running in a Local Git Worktree",
|
||||
};
|
||||
|
||||
// Deduplicate chips by name — e.g. two paths both named
|
||||
// "olivetti" produce a single chip. Highlight positions
|
||||
// come from the first occurrence.
|
||||
let mut seen_names: Vec<SharedString> = Vec::new();
|
||||
let mut worktree_labels: Vec<AnyElement> = Vec::new();
|
||||
|
||||
for wt in self.worktrees {
|
||||
if seen_names.contains(&wt.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if wt.kind == WorktreeKind::Main {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chip_index = seen_names.len();
|
||||
seen_names.push(wt.name.clone());
|
||||
|
||||
let label = if wt.highlight_positions.is_empty() {
|
||||
Label::new(wt.name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
} else {
|
||||
HighlightedLabel::new(wt.name, wt.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
};
|
||||
let tooltip_title = worktree_tooltip_title;
|
||||
let tooltip_meta = worktree_tooltip.clone();
|
||||
|
||||
worktree_labels.push(
|
||||
h_flex()
|
||||
.id(format!("{}-worktree-{chip_index}", self.id.clone()))
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(IconName::GitWorktree)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(label)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
tooltip_title,
|
||||
None,
|
||||
tooltip_meta.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
this.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.gap_1p5()
|
||||
.child(icon_container()) // Icon Spacing
|
||||
.when_some(self.project_name, |this, name| {
|
||||
this.child(
|
||||
Label::new(name).size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
has_project_name && (has_project_paths || has_worktree),
|
||||
|this| this.child(dot_separator()),
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.flex_shrink()
|
||||
.overflow_hidden()
|
||||
.gap_1p5()
|
||||
.when_some(self.project_name, |this, name| {
|
||||
this.child(
|
||||
Label::new(name)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
has_project_name && (has_project_paths || has_worktree),
|
||||
|this| this.child(dot_separator()),
|
||||
)
|
||||
.when_some(project_paths, |this, paths| {
|
||||
this.child(
|
||||
Label::new(paths)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.when(has_project_paths && has_worktree, |this| {
|
||||
this.child(dot_separator())
|
||||
})
|
||||
.children(worktree_labels),
|
||||
)
|
||||
.when_some(project_paths, |this, paths| {
|
||||
this.child(
|
||||
Label::new(paths)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.when(has_project_paths && has_worktree, |this| {
|
||||
this.child(dot_separator())
|
||||
})
|
||||
.children(worktree_labels)
|
||||
.when(
|
||||
(has_project_name || has_project_paths || has_worktree)
|
||||
&& (has_diff_stats || has_timestamp),
|
||||
@@ -648,6 +694,7 @@ impl Component for ThreadItem {
|
||||
full_path: "link-agent-panel".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: None,
|
||||
}]),
|
||||
)
|
||||
.into_any_element(),
|
||||
@@ -675,6 +722,7 @@ impl Component for ThreadItem {
|
||||
full_path: "my-project".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: None,
|
||||
}])
|
||||
.added(42)
|
||||
.removed(17)
|
||||
@@ -682,6 +730,63 @@ impl Component for ThreadItem {
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Worktree + Branch + Changes + Timestamp",
|
||||
container()
|
||||
.child(
|
||||
ThreadItem::new("ti-5c", "Full metadata with branch")
|
||||
.icon(IconName::AiClaude)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "my-project".into(),
|
||||
full_path: "/worktrees/my-project/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("feature-branch".into()),
|
||||
}])
|
||||
.added(42)
|
||||
.removed(17)
|
||||
.timestamp("3w"),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Long Branch + Changes (truncation)",
|
||||
container()
|
||||
.child(
|
||||
ThreadItem::new("ti-5d", "Metadata overflow with long branch name")
|
||||
.icon(IconName::AiClaude)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "my-project".into(),
|
||||
full_path: "/worktrees/my-project/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("fix-very-long-branch-name-here".into()),
|
||||
}])
|
||||
.added(108)
|
||||
.removed(53)
|
||||
.timestamp("2d"),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Main Branch + Changes + Timestamp",
|
||||
container()
|
||||
.child(
|
||||
ThreadItem::new("ti-5e", "Main worktree branch with diff stats")
|
||||
.icon(IconName::ZedAgent)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "zed".into(),
|
||||
full_path: "/projects/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Main,
|
||||
branch_name: Some("sidebar-show-branch-name".into()),
|
||||
}])
|
||||
.added(23)
|
||||
.removed(8)
|
||||
.timestamp("5m"),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Selected Item",
|
||||
container()
|
||||
@@ -755,6 +860,7 @@ impl Component for ThreadItem {
|
||||
full_path: "my-project-name".into(),
|
||||
highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: None,
|
||||
}]),
|
||||
)
|
||||
.into_any_element(),
|
||||
|
||||
@@ -446,6 +446,23 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
|
||||
}
|
||||
}
|
||||
|
||||
// Run Test: ThreadItem branch names visual test
|
||||
println!("\n--- Test: thread_item_branch_names ---");
|
||||
match run_thread_item_branch_name_visual_tests(app_state.clone(), &mut cx, update_baseline) {
|
||||
Ok(TestResult::Passed) => {
|
||||
println!("✓ thread_item_branch_names: PASSED");
|
||||
passed += 1;
|
||||
}
|
||||
Ok(TestResult::BaselineUpdated(_)) => {
|
||||
println!("✓ thread_item_branch_names: Baseline updated");
|
||||
updated += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("✗ thread_item_branch_names: FAILED - {}", e);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Run Test 3: Multi-workspace sidebar visual tests
|
||||
println!("\n--- Test 3: multi_workspace_sidebar ---");
|
||||
match run_multi_workspace_sidebar_visual_tests(app_state.clone(), &mut cx, update_baseline) {
|
||||
@@ -2849,6 +2866,259 @@ impl gpui::Render for ErrorWrappingTestView {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct ThreadItemBranchNameTestView;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl gpui::Render for ThreadItemBranchNameTestView {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
use ui::{
|
||||
IconName, Label, LabelSize, ThreadItem, ThreadItemWorktreeInfo, WorktreeKind,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let section_label = |text: &str| {
|
||||
Label::new(text.to_string())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
};
|
||||
|
||||
let container = || {
|
||||
v_flex()
|
||||
.w_80()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.child(
|
||||
Label::new("ThreadItem Branch Names")
|
||||
.size(LabelSize::Large)
|
||||
.color(Color::Default),
|
||||
)
|
||||
.child(section_label(
|
||||
"Linked worktree with branch (worktree / branch)",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-linked-branch", "Fix scrolling behavior")
|
||||
.icon(IconName::AiClaude)
|
||||
.timestamp("5m")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "jade-glen".into(),
|
||||
full_path: "/worktrees/jade-glen/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("fix-scrolling".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label(
|
||||
"Linked worktree without branch (detached HEAD)",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-linked-no-branch", "Review worktree cleanup")
|
||||
.icon(IconName::AiClaude)
|
||||
.timestamp("1h")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "focal-arrow".into(),
|
||||
full_path: "/worktrees/focal-arrow/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: None,
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label(
|
||||
"Main worktree with branch (branch only, no icon)",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-main-branch", "Request for Long Classic Poem")
|
||||
.icon(IconName::ZedAgent)
|
||||
.timestamp("2d")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "zed".into(),
|
||||
full_path: "/projects/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Main,
|
||||
branch_name: Some("main".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label(
|
||||
"Main worktree without branch (nothing shown)",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-main-no-branch", "Simple greeting thread")
|
||||
.icon(IconName::ZedAgent)
|
||||
.timestamp("3d")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "zed".into(),
|
||||
full_path: "/projects/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Main,
|
||||
branch_name: None,
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label("Linked worktree where name matches branch"))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-same-name", "Implement feature")
|
||||
.icon(IconName::AiClaude)
|
||||
.timestamp("6d")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "stoic-reed".into(),
|
||||
full_path: "/worktrees/stoic-reed/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("stoic-reed".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label(
|
||||
"Manually opened linked worktree (main_path resolves to original repo)",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-manual-linked", "Robust Git Worktree Rollback")
|
||||
.icon(IconName::ZedAgent)
|
||||
.timestamp("40m")
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "focal-arrow".into(),
|
||||
full_path: "/worktrees/focal-arrow/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("persist-worktree-3-wiring".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label(
|
||||
"Linked worktree + branch + diff stats + timestamp",
|
||||
))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-linked-full", "Full metadata with diff stats")
|
||||
.icon(IconName::AiClaude)
|
||||
.timestamp("3w")
|
||||
.added(42)
|
||||
.removed(17)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "jade-glen".into(),
|
||||
full_path: "/worktrees/jade-glen/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some("feature-branch".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label("Long branch name truncation with diff stats"))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-long-branch", "Overflow test with very long branch")
|
||||
.icon(IconName::AiClaude)
|
||||
.timestamp("2d")
|
||||
.added(108)
|
||||
.removed(53)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "my-project".into(),
|
||||
full_path: "/worktrees/my-project/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
branch_name: Some(
|
||||
"fix-very-long-branch-name-that-should-truncate".into(),
|
||||
),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
.child(section_label("Main branch + diff stats + timestamp"))
|
||||
.child(
|
||||
container().child(
|
||||
ThreadItem::new("ti-main-full", "Main worktree with everything")
|
||||
.icon(IconName::ZedAgent)
|
||||
.timestamp("5m")
|
||||
.added(23)
|
||||
.removed(8)
|
||||
.worktrees(vec![ThreadItemWorktreeInfo {
|
||||
name: "zed".into(),
|
||||
full_path: "/projects/zed".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Main,
|
||||
branch_name: Some("sidebar-show-branch-name".into()),
|
||||
}]),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn run_thread_item_branch_name_visual_tests(
|
||||
_app_state: Arc<AppState>,
|
||||
cx: &mut VisualTestAppContext,
|
||||
update_baseline: bool,
|
||||
) -> Result<TestResult> {
|
||||
let window_size = size(px(400.0), px(1150.0));
|
||||
let bounds = Bounds {
|
||||
origin: point(px(0.0), px(0.0)),
|
||||
size: window_size,
|
||||
};
|
||||
|
||||
let window = cx
|
||||
.update(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
focus: false,
|
||||
show: false,
|
||||
..Default::default()
|
||||
},
|
||||
|_window, cx| cx.new(|_| ThreadItemBranchNameTestView),
|
||||
)
|
||||
})
|
||||
.context("Failed to open thread item branch name test window")?;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_window(window.into(), |_, window, _cx| {
|
||||
window.refresh();
|
||||
})?;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let test_result = run_visual_test(
|
||||
"thread_item_branch_names",
|
||||
window.into(),
|
||||
cx,
|
||||
update_baseline,
|
||||
)?;
|
||||
|
||||
cx.update_window(window.into(), |_, window, _cx| {
|
||||
window.remove_window();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
for _ in 0..15 {
|
||||
cx.advance_clock(Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
Ok(test_result)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct ThreadItemIconDecorationsTestView;
|
||||
|
||||
|
||||
Reference in new issue
Block a user