@@ -70,6 +70,7 @@ enum ListEntry {
ProjectHeader {
path_list: PathList,
label: SharedString,
+ workspace_index: usize,
highlight_positions: Vec<usize>,
},
Thread {
@@ -539,6 +540,7 @@ impl Sidebar {
entries.push(ListEntry::ProjectHeader {
path_list: path_list.clone(),
label,
+ workspace_index: index,
highlight_positions: workspace_highlight_positions,
});
entries.extend(matched_threads);
@@ -546,6 +548,7 @@ impl Sidebar {
entries.push(ListEntry::ProjectHeader {
path_list: path_list.clone(),
label,
+ workspace_index: index,
highlight_positions: Vec::new(),
});
@@ -652,11 +655,13 @@ impl Sidebar {
ListEntry::ProjectHeader {
path_list,
label,
+ workspace_index,
highlight_positions,
} => self.render_project_header(
ix,
path_list,
label,
+ *workspace_index,
highlight_positions,
is_selected,
cx,
@@ -706,6 +711,7 @@ impl Sidebar {
ix: usize,
path_list: &PathList,
label: &SharedString,
+ workspace_index: usize,
highlight_positions: &[usize],
is_selected: bool,
cx: &mut Context<Self>,
@@ -736,6 +742,25 @@ impl Sidebar {
.px_1()
.py_1p5()
.gap_0p5()
+ .child(
+ IconButton::new(
+ SharedString::from(format!("project-header-chevron-{}", ix)),
+ disclosure_icon,
+ )
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .shape(IconButtonShape::Square)
+ .tooltip(Tooltip::text(if is_collapsed {
+ "Expand"
+ } else {
+ "Collapse"
+ }))
+ .on_click(cx.listener(
+ move |this, _, window, cx| {
+ this.toggle_collapse(&path_list_for_toggle, window, cx);
+ },
+ )),
+ )
.child(if highlight_positions.is_empty() {
Label::new(label.clone())
.size(LabelSize::Small)
@@ -746,14 +771,7 @@ impl Sidebar {
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
- })
- .child(
- div().visible_on_hover(group).child(
- Icon::new(disclosure_icon)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
- ),
+ }),
)
.end_hover_slot(
h_flex()
@@ -787,11 +805,26 @@ impl Sidebar {
)
.on_click(cx.listener(move |this, _, window, cx| {
this.selection = None;
- this.toggle_collapse(&path_list_for_toggle, window, cx);
+ this.activate_workspace(workspace_index, window, cx);
}))
.into_any_element()
}
+ fn activate_workspace(
+ &mut self,
+ workspace_index: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(multi_workspace) = self.multi_workspace.upgrade() else {
+ return;
+ };
+
+ multi_workspace.update(cx, |multi_workspace, cx| {
+ multi_workspace.activate_index(workspace_index, window, cx);
+ });
+ }
+
fn remove_workspace(
&mut self,
path_list: &PathList,
@@ -915,9 +948,11 @@ impl Sidebar {
};
match entry {
- ListEntry::ProjectHeader { path_list, .. } => {
- let path_list = path_list.clone();
- self.toggle_collapse(&path_list, window, cx);
+ ListEntry::ProjectHeader {
+ workspace_index, ..
+ } => {
+ let workspace_index = *workspace_index;
+ self.activate_workspace(workspace_index, window, cx);
}
ListEntry::Thread {
session_info,
@@ -1797,6 +1832,7 @@ mod tests {
ListEntry::ProjectHeader {
path_list: expanded_path.clone(),
label: "expanded-project".into(),
+ workspace_index: 0,
highlight_positions: Vec::new(),
},
// Thread with default (Completed) status, not active
@@ -1898,6 +1934,7 @@ mod tests {
ListEntry::ProjectHeader {
path_list: collapsed_path.clone(),
label: "collapsed-project".into(),
+ workspace_index: 1,
highlight_positions: Vec::new(),
},
];
@@ -2044,12 +2081,17 @@ mod tests {
}
#[gpui::test]
- async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestAppContext) {
+ async fn test_keyboard_confirm_on_project_header_activates_workspace(cx: &mut TestAppContext) {
let project = init_test_project("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let sidebar = setup_sidebar(&multi_workspace, cx);
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.create_workspace(window, cx);
+ });
+ cx.run_until_parked();
+
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
save_n_test_threads(1, &path_list, cx).await;
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
@@ -2057,29 +2099,35 @@ mod tests {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec!["v [my-project]", " Thread 1"]
+ vec![
+ "v [my-project]",
+ " Thread 1",
+ "v [Empty Workspace]",
+ " [+ New Thread]",
+ ]
);
- // Focus the sidebar — focus_in selects the header (index 0)
- open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
- assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
-
- // Press confirm to collapse
- cx.dispatch_action(Confirm);
+ // Switch to workspace 1 so we can verify confirm switches back.
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ mw.activate_index(1, window, cx);
+ });
cx.run_until_parked();
-
assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec!["> [my-project] <== selected"]
+ multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+ 1
);
- // Confirm again to expand
+ // Focus the sidebar — focus_in selects the header (index 0)
+ open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
+ assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
+
+ // Press confirm on project header (workspace 0) to activate it.
cx.dispatch_action(Confirm);
cx.run_until_parked();
assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec!["v [my-project] <== selected", " Thread 1",]
+ multi_workspace.read_with(cx, |mw, _| mw.active_workspace_index()),
+ 0
);
}
@@ -2859,10 +2907,10 @@ mod tests {
cx.run_until_parked();
// User focuses the sidebar and collapses the group using keyboard:
- // select the header, then press Confirm to toggle collapse.
+ // select the header, then press CollapseSelectedEntry to collapse.
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
- cx.dispatch_action(Confirm);
+ cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
assert_eq!(