diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 082f99cc6dabc6c4d65e6895fc7b6f2ce023d54c..6b109b98ff3ffff0ee4d7661664a06306c1c7d45 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -899,7 +899,14 @@ impl Pane { window: &mut Window, cx: &mut Context, ) { - self.close_items_over_max_tabs(window, cx); + let item_already_exists = self + .items + .iter() + .any(|existing_item| existing_item.item_id() == item.item_id()); + + if !item_already_exists { + self.close_items_over_max_tabs(window, cx); + } if item.is_singleton(cx) { if let Some(&entry_id) = item.project_entry_ids(cx).first() { @@ -1404,6 +1411,9 @@ impl Pane { if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) { continue; } + if self.is_tab_pinned(index) { + continue; + } index_list.push(index); items_len -= 1; @@ -2053,13 +2063,15 @@ impl Pane { self.set_preview_item_id(None, cx); } - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |_, window, cx| { - move_item(&pane, &pane, id, destination_index, window, cx) - }); - }) - .ok()?; + if ix != destination_index { + self.workspace + .update(cx, |_, cx| { + cx.defer_in(window, move |_, window, cx| { + move_item(&pane, &pane, id, destination_index, window, cx) + }); + }) + .ok()?; + } Some(()) }); @@ -3789,6 +3801,288 @@ mod tests { ); } + #[gpui::test] + async fn test_allow_pinning_dirty_item_at_max_tabs(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(1)); + let item_a = add_labeled_item(&pane, "A", true, cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A*^!"], cx); + } + + #[gpui::test] + async fn test_allow_pinning_non_dirty_item_at_max_tabs(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(1)); + let item_a = add_labeled_item(&pane, "A", false, cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A*!"], cx); + } + + #[gpui::test] + async fn test_pin_tabs_incrementally_at_max_capacity(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(3)); + + let item_a = add_labeled_item(&pane, "A", false, cx); + assert_item_labels(&pane, ["A*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A*!"], cx); + + let item_b = add_labeled_item(&pane, "B", false, cx); + assert_item_labels(&pane, ["A!", "B*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B*!"], cx); + + let item_c = add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A!", "B!", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); + } + + #[gpui::test] + async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(3)); + + let item_a = add_labeled_item(&pane, "A", false, cx); + assert_item_labels(&pane, ["A*"], cx); + + let item_b = add_labeled_item(&pane, "B", false, cx); + assert_item_labels(&pane, ["A", "B*"], cx); + + let item_c = add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B!", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); + } + + #[gpui::test] + async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(3)); + + let item_a = add_labeled_item(&pane, "A", false, cx); + assert_item_labels(&pane, ["A*"], cx); + + let item_b = add_labeled_item(&pane, "B", false, cx); + assert_item_labels(&pane, ["A", "B*"], cx); + + let item_c = add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["C*!", "A", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["C!", "B*!", "A"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["C!", "B*!", "A!"], cx); + } + + #[gpui::test] + async fn test_pinned_tabs_never_closed_at_max_tabs(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + let item_a = add_labeled_item(&pane, "A", false, cx); + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + + let item_b = add_labeled_item(&pane, "B", false, cx); + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + + add_labeled_item(&pane, "C", false, cx); + add_labeled_item(&pane, "D", false, cx); + add_labeled_item(&pane, "E", false, cx); + assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); + + set_max_tabs(cx, Some(3)); + add_labeled_item(&pane, "F", false, cx); + assert_item_labels(&pane, ["A!", "B!", "F*"], cx); + + add_labeled_item(&pane, "G", false, cx); + assert_item_labels(&pane, ["A!", "B!", "G*"], cx); + + add_labeled_item(&pane, "H", false, cx); + assert_item_labels(&pane, ["A!", "B!", "H*"], cx); + } + + #[gpui::test] + async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count( + cx: &mut TestAppContext, + ) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(3)); + + let item_a = add_labeled_item(&pane, "A", false, cx); + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + + let item_b = add_labeled_item(&pane, "B", false, cx); + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + + let item_c = add_labeled_item(&pane, "C", false, cx); + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + + assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); + + let item_d = add_labeled_item(&pane, "D", false, cx); + assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx); + + pane.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx); + + add_labeled_item(&pane, "E", false, cx); + assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx); + + add_labeled_item(&pane, "F", false, cx); + assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx); + } + + #[gpui::test] + async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + set_max_tabs(cx, Some(3)); + + add_labeled_item(&pane, "A", true, cx); + assert_item_labels(&pane, ["A*^"], cx); + + add_labeled_item(&pane, "B", true, cx); + assert_item_labels(&pane, ["A^", "B*^"], cx); + + add_labeled_item(&pane, "C", true, cx); + assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); + + add_labeled_item(&pane, "D", false, cx); + assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx); + + add_labeled_item(&pane, "E", false, cx); + assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx); + + add_labeled_item(&pane, "F", false, cx); + assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx); + + add_labeled_item(&pane, "G", true, cx); + assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx); + } + #[gpui::test] async fn test_add_item_with_new_item(cx: &mut TestAppContext) { init_test(cx);