diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9fad4e8a5d0a3909ebaa2279cf274e213675ede1..7f28abdd03242000e473bbf94f31fa41c9c9f6d7 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2174,10 +2174,6 @@ impl Pane { self.pinned_tab_count > ix } - fn has_pinned_tabs(&self) -> bool { - self.pinned_tab_count != 0 - } - fn has_unpinned_tabs(&self) -> bool { self.pinned_tab_count < self.items.len() } @@ -2900,8 +2896,11 @@ impl Pane { to_pane = workspace.split_pane(to_pane, split_direction, window, cx); } let database_id = workspace.database_id(); - let old_ix = from_pane.read(cx).index_for_item_id(item_id); - let old_len = to_pane.read(cx).items.len(); + let from_old_ix = from_pane.read(cx).index_for_item_id(item_id); + let was_pinned = from_old_ix + .map(|ix| from_pane.read(cx).is_tab_pinned(ix)) + .unwrap_or(false); + let to_pane_old_length = to_pane.read(cx).items.len(); if is_clone { let Some(item) = from_pane .read(cx) @@ -2919,38 +2918,22 @@ impl Pane { } else { move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); } - if to_pane == from_pane { - if let Some(old_index) = old_ix { - to_pane.update(cx, |this, _| { - if old_index < this.pinned_tab_count - && (ix == this.items.len() || ix > this.pinned_tab_count) - { - this.pinned_tab_count -= 1; - } else if this.has_pinned_tabs() - && old_index >= this.pinned_tab_count - && ix < this.pinned_tab_count - { - this.pinned_tab_count += 1; - } - }); - } - } else { - to_pane.update(cx, |this, _| { - if this.items.len() > old_len // Did we not deduplicate on drag? - && this.has_pinned_tabs() - && ix < this.pinned_tab_count - { + to_pane.update(cx, |this, _| { + let now_in_pinned_region = ix < this.pinned_tab_count; + if to_pane == from_pane { + if was_pinned && !now_in_pinned_region { + this.pinned_tab_count -= 1; + } else if !was_pinned && now_in_pinned_region { this.pinned_tab_count += 1; } - }); - from_pane.update(cx, |this, _| { - if let Some(index) = old_ix { - if this.pinned_tab_count > index { - this.pinned_tab_count -= 1; - } + } else if this.items.len() > to_pane_old_length { + if to_pane_old_length == 0 && was_pinned { + this.pinned_tab_count = 1; + } else if now_in_pinned_region { + this.pinned_tab_count += 1; } - }) - } + } + }); }); }) .log_err(); @@ -4325,6 +4308,280 @@ mod tests { assert_item_labels(&pane, ["C", "A", "B*"], cx); } + #[gpui::test] + async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab( + 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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add A, B. Pin B. Activate A + let item_a = add_labeled_item(&pane_a, "A", false, cx); + let item_b = add_labeled_item(&pane_a, "B", false, cx); + + pane_a.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 ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.activate_item(ix, true, true, window, cx); + }); + + // Drag A to create new split + pane_a.update_in(cx, |pane, window, cx| { + pane.drag_split_direction = Some(SplitDirection::Right); + + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 0, window, cx); + }); + + // A should be moved to new pane. B should remain pinned, A should not be pinned + let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { + let panes = workspace.panes(); + (panes[0].clone(), panes[1].clone()) + }); + assert_item_labels(&pane_a, ["B*!"], cx); + assert_item_labels(&pane_b, ["A*"], cx); + } + + #[gpui::test] + async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add A, B. Pin both. Activate A + let item_a = add_labeled_item(&pane_a, "A", false, cx); + let item_b = add_labeled_item(&pane_a, "B", false, cx); + + pane_a.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 ix = pane.index_for_item_id(item_b.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.activate_item(ix, true, true, window, cx); + }); + assert_item_labels(&pane_a, ["A*!", "B!"], cx); + + // Drag A to create new split + pane_a.update_in(cx, |pane, window, cx| { + pane.drag_split_direction = Some(SplitDirection::Right); + + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 0, window, cx); + }); + + // A should be moved to new pane. Both A and B should still be pinned + let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { + let panes = workspace.panes(); + (panes[0].clone(), panes[1].clone()) + }); + assert_item_labels(&pane_a, ["B*!"], cx); + assert_item_labels(&pane_b, ["A*!"], cx); + } + + #[gpui::test] + async fn test_drag_pinned_tab_into_existing_panes_pinned_region(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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add A to pane A and pin + let item_a = add_labeled_item(&pane_a, "A", false, cx); + pane_a.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, ["A*!"], cx); + + // Add B to pane B and pin + let pane_b = workspace.update_in(cx, |workspace, window, cx| { + workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) + }); + let item_b = add_labeled_item(&pane_b, "B", false, cx); + pane_b.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_b, ["B*!"], cx); + + // Move A from pane A to pane B's pinned region + pane_b.update_in(cx, |pane, window, cx| { + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 0, window, cx); + }); + + // A should stay pinned + assert_item_labels(&pane_a, [], cx); + assert_item_labels(&pane_b, ["A*!", "B!"], cx); + } + + #[gpui::test] + async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add A to pane A and pin + let item_a = add_labeled_item(&pane_a, "A", false, cx); + pane_a.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, ["A*!"], cx); + + // Create pane B with pinned item B + let pane_b = workspace.update_in(cx, |workspace, window, cx| { + workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) + }); + let item_b = add_labeled_item(&pane_b, "B", false, cx); + assert_item_labels(&pane_b, ["B*"], cx); + + pane_b.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_b, ["B*!"], cx); + + // Move A from pane A to pane B's unpinned region + pane_b.update_in(cx, |pane, window, cx| { + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 1, window, cx); + }); + + // A should become pinned + assert_item_labels(&pane_a, [], cx); + assert_item_labels(&pane_b, ["B!", "A*"], cx); + } + + #[gpui::test] + async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add unpinned item A to pane A + let item_a = add_labeled_item(&pane_a, "A", false, cx); + assert_item_labels(&pane_a, ["A*"], cx); + + // Create pane B with pinned item B + let pane_b = workspace.update_in(cx, |workspace, window, cx| { + workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) + }); + let item_b = add_labeled_item(&pane_b, "B", false, cx); + pane_b.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_b, ["B*!"], cx); + + // Move A from pane A to pane B's pinned region + pane_b.update_in(cx, |pane, window, cx| { + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 0, window, cx); + }); + + // A should become pinned since it was dropped in the pinned region + assert_item_labels(&pane_a, [], cx); + assert_item_labels(&pane_b, ["A*!", "B!"], cx); + } + + #[gpui::test] + async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(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_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add unpinned item A to pane A + let item_a = add_labeled_item(&pane_a, "A", false, cx); + assert_item_labels(&pane_a, ["A*"], cx); + + // Create pane B with one pinned item B + let pane_b = workspace.update_in(cx, |workspace, window, cx| { + workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) + }); + let item_b = add_labeled_item(&pane_b, "B", false, cx); + pane_b.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_b, ["B*!"], cx); + + // Move A from pane A to pane B's unpinned region + pane_b.update_in(cx, |pane, window, cx| { + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 1, window, cx); + }); + + // A should remain unpinned since it was dropped outside the pinned region + assert_item_labels(&pane_a, [], cx); + assert_item_labels(&pane_b, ["B!", "A*"], cx); + } + #[gpui::test] async fn test_add_item_with_new_item(cx: &mut TestAppContext) { init_test(cx);