mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
sidebar: Fixes around multi-root projects (#53276)
When a project mixes main repo worktrees (e.g. `extensions`) with linked git worktrees (e.g. `zed4`, a linked worktree of `zed`), two things were broken: 1. **Threads appeared under the wrong sidebar heading.** A thread created in a workspace with both `extensions` and `zed4` would show under the "zed" group instead of the "extensions, zed" group. This happened because `main_worktree_paths` in `ThreadMetadata` was only populated with paths from linked worktrees, omitting regular repos entirely. The fix uses `project_group_key()` to normalize all visible worktrees to their main repo paths — the same normalization the sidebar uses for group headers. 2. **The worktree chip tooltip only showed linked worktree paths**, missing main repo paths like `extensions`. This happened because `worktree_info_from_thread_paths` filtered out main worktree paths entirely. The fix introduces a `WorktreeKind` enum (`Main` / `Linked`) on `ThreadItemWorktreeInfo`, so all worktrees are included in the data model. Chips still only render for `Linked` worktrees (main worktrees are redundant with the group header), but the tooltip now shows all paths. Release Notes: - N/A
This commit is contained in:
parent
70d6c2bdc4
commit
082950d878
4 changed files with 79 additions and 75 deletions
|
|
@ -567,19 +567,12 @@ impl ThreadMetadataStore {
|
|||
PathList::new(&paths)
|
||||
};
|
||||
|
||||
let main_worktree_paths = {
|
||||
let project = thread_ref.project().read(cx);
|
||||
let mut main_paths: Vec<Arc<Path>> = Vec::new();
|
||||
for repo in project.repositories(cx).values() {
|
||||
let snapshot = repo.read(cx).snapshot();
|
||||
if snapshot.is_linked_worktree() {
|
||||
main_paths.push(snapshot.original_repo_abs_path.clone());
|
||||
}
|
||||
}
|
||||
main_paths.sort();
|
||||
main_paths.dedup();
|
||||
PathList::new(&main_paths)
|
||||
};
|
||||
let main_worktree_paths = thread_ref
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_group_key(cx)
|
||||
.path_list()
|
||||
.clone();
|
||||
|
||||
// Threads without a folder path (e.g. started in an empty
|
||||
// window) are archived by default so they don't get lost,
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ struct WorktreeInfo {
|
|||
name: SharedString,
|
||||
full_path: SharedString,
|
||||
highlight_positions: Vec<usize>,
|
||||
kind: ui::WorktreeKind,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -307,23 +308,25 @@ fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
|
|||
|
||||
/// Derives worktree display info from a thread's stored path list.
|
||||
///
|
||||
/// For each path in the thread's `folder_paths` that is not one of the
|
||||
/// group's main paths (i.e. it's a git linked worktree), produces a
|
||||
/// [`WorktreeInfo`] with the short worktree name and full path.
|
||||
/// 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.
|
||||
fn worktree_info_from_thread_paths(
|
||||
folder_paths: &PathList,
|
||||
group_key: &project::ProjectGroupKey,
|
||||
) -> Vec<WorktreeInfo> {
|
||||
) -> impl Iterator<Item = WorktreeInfo> {
|
||||
let main_paths = group_key.path_list().paths();
|
||||
folder_paths
|
||||
.paths()
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if main_paths.iter().any(|mp| mp.as_path() == path.as_path()) {
|
||||
return None;
|
||||
}
|
||||
// Find the main path whose file name matches this linked
|
||||
// worktree's file name, falling back to the first main path.
|
||||
folder_paths.paths().iter().filter_map(|path| {
|
||||
let is_main = main_paths.iter().any(|mp| mp.as_path() == path.as_path());
|
||||
if is_main {
|
||||
let name = path.file_name()?.to_string_lossy().to_string();
|
||||
Some(WorktreeInfo {
|
||||
name: SharedString::from(name),
|
||||
full_path: SharedString::from(path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Main,
|
||||
})
|
||||
} else {
|
||||
let main_path = main_paths
|
||||
.iter()
|
||||
.find(|mp| mp.file_name() == path.file_name())
|
||||
|
|
@ -332,9 +335,10 @@ fn worktree_info_from_thread_paths(
|
|||
name: linked_worktree_short_name(main_path, path).unwrap_or_default(),
|
||||
full_path: SharedString::from(path.display().to_string()),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: ui::WorktreeKind::Linked,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The sidebar re-derives its entire entry list from scratch on every
|
||||
|
|
@ -851,7 +855,8 @@ impl Sidebar {
|
|||
workspace: ThreadEntryWorkspace|
|
||||
-> ThreadEntry {
|
||||
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
|
||||
let worktrees = worktree_info_from_thread_paths(&row.folder_paths, &group_key);
|
||||
let worktrees: Vec<WorktreeInfo> =
|
||||
worktree_info_from_thread_paths(&row.folder_paths, &group_key).collect();
|
||||
ThreadEntry {
|
||||
metadata: row,
|
||||
icon,
|
||||
|
|
@ -1059,7 +1064,9 @@ impl Sidebar {
|
|||
if let Some(ActiveEntry::Draft(draft_ws)) = &self.active_entry {
|
||||
let ws_path_list = workspace_path_list(draft_ws, cx);
|
||||
let worktrees = worktree_info_from_thread_paths(&ws_path_list, &group_key);
|
||||
entries.push(ListEntry::DraftThread { worktrees });
|
||||
entries.push(ListEntry::DraftThread {
|
||||
worktrees: worktrees.collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1073,7 +1080,8 @@ impl Sidebar {
|
|||
&& active_workspace.as_ref().is_some_and(|active_ws| {
|
||||
let ws_path_list = workspace_path_list(active_ws, cx);
|
||||
let has_linked_worktrees =
|
||||
!worktree_info_from_thread_paths(&ws_path_list, &group_key).is_empty();
|
||||
worktree_info_from_thread_paths(&ws_path_list, &group_key)
|
||||
.any(|wt| wt.kind == ui::WorktreeKind::Linked);
|
||||
if !has_linked_worktrees {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1102,6 +1110,7 @@ impl Sidebar {
|
|||
&workspace_path_list(ws, cx),
|
||||
&group_key,
|
||||
)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
|
|
@ -2545,6 +2554,7 @@ impl Sidebar {
|
|||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: wt.kind,
|
||||
})
|
||||
.collect(),
|
||||
diff_stats: thread.diff_stats,
|
||||
|
|
@ -2817,6 +2827,7 @@ impl Sidebar {
|
|||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: wt.highlight_positions.clone(),
|
||||
kind: wt.kind,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
|
|
@ -3095,6 +3106,7 @@ impl Sidebar {
|
|||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: wt.highlight_positions.clone(),
|
||||
kind: wt.kind,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
|
|
@ -3132,6 +3144,7 @@ impl Sidebar {
|
|||
name: wt.name.clone(),
|
||||
full_path: wt.full_path.clone(),
|
||||
highlight_positions: wt.highlight_positions.clone(),
|
||||
kind: wt.kind,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -191,6 +191,25 @@ fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
|
|||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String {
|
||||
let mut seen = Vec::new();
|
||||
let mut chips = Vec::new();
|
||||
for wt in worktrees {
|
||||
if wt.kind == ui::WorktreeKind::Main {
|
||||
continue;
|
||||
}
|
||||
if !seen.contains(&wt.name) {
|
||||
seen.push(wt.name.clone());
|
||||
chips.push(format!("{{{}}}", wt.name));
|
||||
}
|
||||
}
|
||||
if chips.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", chips.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_entries_as_strings(
|
||||
sidebar: &Entity<Sidebar>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
|
|
@ -238,23 +257,8 @@ fn visible_entries_as_strings(
|
|||
} else {
|
||||
""
|
||||
};
|
||||
let worktree = if thread.worktrees.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut seen = Vec::new();
|
||||
let mut chips = Vec::new();
|
||||
for wt in &thread.worktrees {
|
||||
if !seen.contains(&wt.name) {
|
||||
seen.push(wt.name.clone());
|
||||
chips.push(format!("{{{}}}", wt.name));
|
||||
}
|
||||
}
|
||||
format!(" {}", chips.join(", "))
|
||||
};
|
||||
format!(
|
||||
" {}{}{}{}{}{}",
|
||||
title, worktree, active, status_str, notified, selected
|
||||
)
|
||||
let worktree = format_linked_worktree_chips(&thread.worktrees);
|
||||
format!(" {title}{worktree}{active}{status_str}{notified}{selected}")
|
||||
}
|
||||
ListEntry::ViewMore {
|
||||
is_fully_expanded, ..
|
||||
|
|
@ -266,35 +270,11 @@ fn visible_entries_as_strings(
|
|||
}
|
||||
}
|
||||
ListEntry::DraftThread { worktrees, .. } => {
|
||||
let worktree = if worktrees.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut seen = Vec::new();
|
||||
let mut chips = Vec::new();
|
||||
for wt in worktrees {
|
||||
if !seen.contains(&wt.name) {
|
||||
seen.push(wt.name.clone());
|
||||
chips.push(format!("{{{}}}", wt.name));
|
||||
}
|
||||
}
|
||||
format!(" {}", chips.join(", "))
|
||||
};
|
||||
let worktree = format_linked_worktree_chips(worktrees);
|
||||
format!(" [~ Draft{}]{}", worktree, selected)
|
||||
}
|
||||
ListEntry::NewThread { worktrees, .. } => {
|
||||
let worktree = if worktrees.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut seen = Vec::new();
|
||||
let mut chips = Vec::new();
|
||||
for wt in worktrees {
|
||||
if !seen.contains(&wt.name) {
|
||||
seen.push(wt.name.clone());
|
||||
chips.push(format!("{{{}}}", wt.name));
|
||||
}
|
||||
}
|
||||
format!(" {}", chips.join(", "))
|
||||
};
|
||||
let worktree = format_linked_worktree_chips(worktrees);
|
||||
format!(" [+ New Thread{}]{}", worktree, selected)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,19 @@ pub enum AgentThreadStatus {
|
|||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum WorktreeKind {
|
||||
#[default]
|
||||
Main,
|
||||
Linked,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadItemWorktreeInfo {
|
||||
pub name: SharedString,
|
||||
pub full_path: SharedString,
|
||||
pub highlight_positions: Vec<usize>,
|
||||
pub kind: WorktreeKind,
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
|
|
@ -359,7 +367,10 @@ 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.is_empty();
|
||||
let has_worktree = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.any(|wt| wt.kind == WorktreeKind::Linked);
|
||||
let has_timestamp = !self.timestamp.is_empty();
|
||||
let timestamp = self.timestamp;
|
||||
|
||||
|
|
@ -449,6 +460,10 @@ impl RenderOnce for ThreadItem {
|
|||
continue;
|
||||
}
|
||||
|
||||
if wt.kind == WorktreeKind::Main {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chip_index = seen_names.len();
|
||||
seen_names.push(wt.name.clone());
|
||||
|
||||
|
|
@ -624,6 +639,7 @@ impl Component for ThreadItem {
|
|||
name: "link-agent-panel".into(),
|
||||
full_path: "link-agent-panel".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
}]),
|
||||
)
|
||||
.into_any_element(),
|
||||
|
|
@ -650,6 +666,7 @@ impl Component for ThreadItem {
|
|||
name: "my-project".into(),
|
||||
full_path: "my-project".into(),
|
||||
highlight_positions: Vec::new(),
|
||||
kind: WorktreeKind::Linked,
|
||||
}])
|
||||
.added(42)
|
||||
.removed(17)
|
||||
|
|
@ -729,6 +746,7 @@ impl Component for ThreadItem {
|
|||
name: "my-project-name".into(),
|
||||
full_path: "my-project-name".into(),
|
||||
highlight_positions: vec![3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
kind: WorktreeKind::Linked,
|
||||
}]),
|
||||
)
|
||||
.into_any_element(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue