Detailed changes
@@ -265,49 +265,72 @@ pub(crate) fn deserialize_pane_layout(
pane.entity_id(),
cx.subscribe_in(&pane, window, RunningState::handle_pane_event),
);
+ let running_state = cx.weak_entity();
+ let pane_handle = pane.downgrade();
let sub_views: Vec<_> = serialized_pane
.children
.iter()
.map(|child| match child {
- DebuggerPaneItem::Frames => {
- Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx))
- }
+ DebuggerPaneItem::Frames => Box::new(SubView::stack_frame_list(
+ stack_frame_list.clone(),
+ running_state.clone(),
+ pane_handle.clone(),
+ cx,
+ )),
DebuggerPaneItem::Variables => Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),
DebuggerPaneItem::Variables,
+ running_state.clone(),
+ pane_handle.clone(),
+ cx,
+ )),
+ DebuggerPaneItem::BreakpointList => Box::new(SubView::breakpoint_list(
+ breakpoint_list.clone(),
+ running_state.clone(),
+ pane_handle.clone(),
cx,
)),
- DebuggerPaneItem::BreakpointList => {
- Box::new(SubView::breakpoint_list(breakpoint_list.clone(), cx))
- }
DebuggerPaneItem::Modules => Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
+ running_state.clone(),
+ pane_handle.clone(),
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
loaded_sources.focus_handle(cx),
loaded_sources.clone().into(),
DebuggerPaneItem::LoadedSources,
+ running_state.clone(),
+ pane_handle.clone(),
cx,
)),
DebuggerPaneItem::Console => {
- let view = SubView::console(console.clone(), cx);
+ let view = SubView::console(
+ console.clone(),
+ running_state.clone(),
+ pane_handle.clone(),
+ cx,
+ );
Box::new(view)
}
DebuggerPaneItem::Terminal => Box::new(SubView::new(
terminal.focus_handle(cx),
terminal.clone().into(),
DebuggerPaneItem::Terminal,
+ running_state.clone(),
+ pane_handle.clone(),
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
memory_view.focus_handle(cx),
memory_view.clone().into(),
DebuggerPaneItem::MemoryView,
+ running_state.clone(),
+ pane_handle.clone(),
cx,
)),
})
@@ -7,7 +7,6 @@ pub mod stack_frame_list;
pub mod variable_list;
use std::{
any::Any,
- ops::ControlFlow,
path::PathBuf,
sync::{Arc, LazyLock},
time::Duration,
@@ -72,6 +71,7 @@ pub struct RunningState {
focus_handle: FocusHandle,
_remote_id: Option<ViewId>,
workspace: WeakEntity<Workspace>,
+ project: WeakEntity<Project>,
session_id: SessionId,
variable_list: Entity<variable_list::VariableList>,
_subscriptions: Vec<Subscription>,
@@ -144,6 +144,8 @@ pub(crate) struct SubView {
inner: AnyView,
item_focus_handle: FocusHandle,
kind: DebuggerPaneItem,
+ running_state: WeakEntity<RunningState>,
+ host_pane: WeakEntity<Pane>,
show_indicator: Box<dyn Fn(&App) -> bool>,
actions: Option<Box<dyn FnMut(&mut Window, &mut App) -> AnyElement>>,
hovered: bool,
@@ -154,12 +156,16 @@ impl SubView {
item_focus_handle: FocusHandle,
view: AnyView,
kind: DebuggerPaneItem,
+ running_state: WeakEntity<RunningState>,
+ host_pane: WeakEntity<Pane>,
cx: &mut App,
) -> Entity<Self> {
cx.new(|_| Self {
kind,
inner: view,
item_focus_handle,
+ running_state,
+ host_pane,
show_indicator: Box::new(|_| false),
actions: None,
hovered: false,
@@ -168,6 +174,8 @@ impl SubView {
pub(crate) fn stack_frame_list(
stack_frame_list: Entity<StackFrameList>,
+ running_state: WeakEntity<RunningState>,
+ host_pane: WeakEntity<Pane>,
cx: &mut App,
) -> Entity<Self> {
let weak_list = stack_frame_list.downgrade();
@@ -175,6 +183,8 @@ impl SubView {
stack_frame_list.focus_handle(cx),
stack_frame_list.into(),
DebuggerPaneItem::Frames,
+ running_state,
+ host_pane,
cx,
);
@@ -189,12 +199,19 @@ impl SubView {
this
}
- pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
+ pub(crate) fn console(
+ console: Entity<Console>,
+ running_state: WeakEntity<RunningState>,
+ host_pane: WeakEntity<Pane>,
+ cx: &mut App,
+ ) -> Entity<Self> {
let weak_console = console.downgrade();
let this = Self::new(
console.focus_handle(cx),
console.into(),
DebuggerPaneItem::Console,
+ running_state,
+ host_pane,
cx,
);
this.update(cx, |this, _| {
@@ -207,13 +224,20 @@ impl SubView {
this
}
- pub(crate) fn breakpoint_list(list: Entity<BreakpointList>, cx: &mut App) -> Entity<Self> {
+ pub(crate) fn breakpoint_list(
+ list: Entity<BreakpointList>,
+ running_state: WeakEntity<RunningState>,
+ host_pane: WeakEntity<Pane>,
+ cx: &mut App,
+ ) -> Entity<Self> {
let weak_list = list.downgrade();
let focus_handle = list.focus_handle(cx);
let this = Self::new(
focus_handle,
list.into(),
DebuggerPaneItem::BreakpointList,
+ running_state,
+ host_pane,
cx,
);
@@ -239,6 +263,10 @@ impl SubView {
) {
self.actions = Some(actions);
}
+
+ fn set_host_pane(&mut self, host_pane: WeakEntity<Pane>) {
+ self.host_pane = host_pane;
+ }
}
impl Focusable for SubView {
fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -281,6 +309,75 @@ impl Item for SubView {
label.into_any_element()
}
+
+ fn handle_drop(
+ &self,
+ active_pane: &Pane,
+ dropped: &dyn Any,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> bool {
+ let Some(tab) = dropped.downcast_ref::<DraggedTab>() else {
+ return true;
+ };
+ let Some(this_pane) = self.host_pane.upgrade() else {
+ return true;
+ };
+ let item = if tab.pane == this_pane {
+ active_pane.item_for_index(tab.ix)
+ } else {
+ tab.pane.read(cx).item_for_index(tab.ix)
+ };
+ let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
+ return true;
+ };
+ let Some(split_direction) = active_pane.drag_split_direction() else {
+ return false;
+ };
+
+ let source = tab.pane.clone();
+ let item_id_to_move = item.item_id();
+ let weak_running = self.running_state.clone();
+
+ // Source pane may be the one currently updated, so defer the move.
+ window.defer(cx, move |window, cx| {
+ let new_pane = weak_running.update(cx, |running, cx| {
+ let Some(project) = running.project.upgrade() else {
+ return Err(anyhow!("Debugger project has been dropped"));
+ };
+
+ let new_pane = new_debugger_pane(running.workspace.clone(), project, window, cx);
+ let _previous_subscription = running.pane_close_subscriptions.insert(
+ new_pane.entity_id(),
+ cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
+ );
+ debug_assert!(_previous_subscription.is_none());
+ running
+ .panes
+ .split(&this_pane, &new_pane, split_direction, cx);
+ anyhow::Ok(new_pane)
+ });
+
+ match new_pane.and_then(|result| result) {
+ Ok(new_pane) => {
+ move_item(
+ &source,
+ &new_pane,
+ item_id_to_move,
+ new_pane.read(cx).active_item_index(),
+ true,
+ window,
+ cx,
+ );
+ }
+ Err(err) => {
+ log::error!("{err:?}");
+ }
+ }
+ });
+
+ true
+ }
}
impl Render for SubView {
@@ -311,83 +408,18 @@ pub(crate) fn new_debugger_pane(
cx: &mut Context<RunningState>,
) -> Entity<Pane> {
let weak_running = cx.weak_entity();
- let custom_drop_handle = {
- let workspace = workspace.clone();
- let project = project.downgrade();
- let weak_running = weak_running.clone();
- move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
- let Some(tab) = any.downcast_ref::<DraggedTab>() else {
- return ControlFlow::Break(());
- };
- let Some(project) = project.upgrade() else {
- return ControlFlow::Break(());
- };
- let this_pane = cx.entity();
- let item = if tab.pane == this_pane {
- pane.item_for_index(tab.ix)
- } else {
- tab.pane.read(cx).item_for_index(tab.ix)
- };
- let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
- return ControlFlow::Break(());
- };
-
- let source = tab.pane.clone();
- let item_id_to_move = item.item_id();
-
- let Some(split_direction) = pane.drag_split_direction() else {
- // If we drop into existing pane or current pane,
- // regular pane drop handler will take care of it,
- // using the right tab index for the operation.
- return ControlFlow::Continue(());
- };
-
- let workspace = workspace.clone();
- let weak_running = weak_running.clone();
- // Source pane may be the one currently updated, so defer the move.
- window.defer(cx, move |window, cx| {
- let new_pane = weak_running.update(cx, |running, cx| {
- let new_pane =
- new_debugger_pane(workspace.clone(), project.clone(), window, cx);
- let _previous_subscription = running.pane_close_subscriptions.insert(
- new_pane.entity_id(),
- cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
- );
- debug_assert!(_previous_subscription.is_none());
- running
- .panes
- .split(&this_pane, &new_pane, split_direction, cx);
- new_pane
- });
-
- match new_pane {
- Ok(new_pane) => {
- move_item(
- &source,
- &new_pane,
- item_id_to_move,
- new_pane.read(cx).active_item_index(),
- true,
- window,
- cx,
- );
- }
- Err(err) => {
- log::error!("{err:?}");
- }
- };
- });
-
- ControlFlow::Break(())
- }
- };
cx.new(move |cx| {
+ let can_drop_predicate: Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool> =
+ Arc::new(|any, _window, _cx| {
+ any.downcast_ref::<DraggedTab>()
+ .is_some_and(|dragged_tab| dragged_tab.item.downcast::<SubView>().is_some())
+ });
let mut pane = Pane::new(
workspace.clone(),
project.clone(),
Default::default(),
- None,
+ Some(can_drop_predicate),
NoAction.boxed_clone(),
true,
window,
@@ -426,7 +458,6 @@ pub(crate) fn new_debugger_pane(
})));
pane.set_can_toggle_zoom(false, cx);
pane.display_nav_history_buttons(None);
- pane.set_custom_drop_handle(cx, custom_drop_handle);
pane.set_should_display_tab_bar(|_, _| true);
pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
pane.set_render_tab_bar(cx, {
@@ -466,8 +497,17 @@ pub(crate) fn new_debugger_pane(
})
.on_drop(cx.listener(
move |this, dragged_tab: &DraggedTab, window, cx| {
+ if dragged_tab.item.downcast::<SubView>().is_none() {
+ return;
+ }
this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
+ this.handle_tab_drop(
+ dragged_tab,
+ this.items_len(),
+ false,
+ window,
+ cx,
+ )
},
))
.children(pane.items().enumerate().map(|(ix, item)| {
@@ -516,8 +556,11 @@ pub(crate) fn new_debugger_pane(
))
.on_drop(cx.listener(
move |this, dragged_tab: &DraggedTab, window, cx| {
+ if dragged_tab.item.downcast::<SubView>().is_none() {
+ return;
+ }
this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, ix, window, cx)
+ this.handle_tab_drop(dragged_tab, ix, false, window, cx)
},
))
.on_drag(
@@ -729,6 +772,7 @@ impl RunningState {
) -> Self {
let focus_handle = cx.focus_handle();
let session_id = session.read(cx).session_id();
+ let weak_project = project.downgrade();
let weak_state = cx.weak_entity();
let stack_frame_list = cx.new(|cx| {
StackFrameList::new(
@@ -904,6 +948,7 @@ impl RunningState {
memory_view,
session,
workspace,
+ project: weak_project,
focus_handle,
variable_list,
_subscriptions,
@@ -1304,48 +1349,71 @@ impl RunningState {
fn create_sub_view(
&self,
item_kind: DebuggerPaneItem,
- _pane: &Entity<Pane>,
+ pane: &Entity<Pane>,
cx: &mut Context<Self>,
) -> Box<dyn ItemHandle> {
+ let running_state = cx.weak_entity();
+ let host_pane = pane.downgrade();
+
match item_kind {
- DebuggerPaneItem::Console => Box::new(SubView::console(self.console.clone(), cx)),
+ DebuggerPaneItem::Console => Box::new(SubView::console(
+ self.console.clone(),
+ running_state,
+ host_pane,
+ cx,
+ )),
DebuggerPaneItem::Variables => Box::new(SubView::new(
self.variable_list.focus_handle(cx),
self.variable_list.clone().into(),
item_kind,
+ running_state,
+ host_pane,
+ cx,
+ )),
+ DebuggerPaneItem::BreakpointList => Box::new(SubView::breakpoint_list(
+ self.breakpoint_list.clone(),
+ running_state,
+ host_pane,
cx,
)),
- DebuggerPaneItem::BreakpointList => {
- Box::new(SubView::breakpoint_list(self.breakpoint_list.clone(), cx))
- }
DebuggerPaneItem::Frames => Box::new(SubView::new(
self.stack_frame_list.focus_handle(cx),
self.stack_frame_list.clone().into(),
item_kind,
+ running_state,
+ host_pane,
cx,
)),
DebuggerPaneItem::Modules => Box::new(SubView::new(
self.module_list.focus_handle(cx),
self.module_list.clone().into(),
item_kind,
+ running_state,
+ host_pane,
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
self.loaded_sources_list.focus_handle(cx),
self.loaded_sources_list.clone().into(),
item_kind,
+ running_state,
+ host_pane,
cx,
)),
DebuggerPaneItem::Terminal => Box::new(SubView::new(
self.debug_terminal.focus_handle(cx),
self.debug_terminal.clone().into(),
item_kind,
+ running_state,
+ host_pane,
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
self.memory_view.focus_handle(cx),
self.memory_view.clone().into(),
item_kind,
+ running_state,
+ host_pane,
cx,
)),
}
@@ -1454,6 +1522,13 @@ impl RunningState {
) {
this.serialize_layout(window, cx);
match event {
+ Event::AddItem { item } => {
+ if let Some(sub_view) = item.downcast::<SubView>() {
+ sub_view.update(cx, |sub_view, _| {
+ sub_view.set_host_pane(source_pane.downgrade());
+ });
+ }
+ }
Event::Remove { .. } => {
let _did_find_pane = this.panes.remove(source_pane, cx).is_ok();
debug_assert!(_did_find_pane);
@@ -1795,23 +1870,28 @@ impl RunningState {
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> Member {
+ let running_state = cx.weak_entity();
+
let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+ let leftmost_pane_handle = leftmost_pane.downgrade();
+ let leftmost_frames = SubView::new(
+ stack_frame_list.focus_handle(cx),
+ stack_frame_list.clone().into(),
+ DebuggerPaneItem::Frames,
+ running_state.clone(),
+ leftmost_pane_handle.clone(),
+ cx,
+ );
+ let leftmost_breakpoints = SubView::breakpoint_list(
+ breakpoints.clone(),
+ running_state.clone(),
+ leftmost_pane_handle,
+ cx,
+ );
leftmost_pane.update(cx, |this, cx| {
+ this.add_item(Box::new(leftmost_frames), true, false, None, window, cx);
this.add_item(
- Box::new(SubView::new(
- this.focus_handle(cx),
- stack_frame_list.clone().into(),
- DebuggerPaneItem::Frames,
- cx,
- )),
- true,
- false,
- None,
- window,
- cx,
- );
- this.add_item(
- Box::new(SubView::breakpoint_list(breakpoints.clone(), cx)),
+ Box::new(leftmost_breakpoints),
true,
false,
None,
@@ -1820,44 +1900,42 @@ impl RunningState {
);
this.activate_item(0, false, false, window, cx);
});
+
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+ let center_pane_handle = center_pane.downgrade();
+ let center_console = SubView::console(
+ console.clone(),
+ running_state.clone(),
+ center_pane_handle.clone(),
+ cx,
+ );
+ let center_variables = SubView::new(
+ variable_list.focus_handle(cx),
+ variable_list.clone().into(),
+ DebuggerPaneItem::Variables,
+ running_state.clone(),
+ center_pane_handle,
+ cx,
+ );
center_pane.update(cx, |this, cx| {
- let view = SubView::console(console.clone(), cx);
+ this.add_item(Box::new(center_console), true, false, None, window, cx);
- this.add_item(Box::new(view), true, false, None, window, cx);
-
- this.add_item(
- Box::new(SubView::new(
- variable_list.focus_handle(cx),
- variable_list.clone().into(),
- DebuggerPaneItem::Variables,
- cx,
- )),
- true,
- false,
- None,
- window,
- cx,
- );
+ this.add_item(Box::new(center_variables), true, false, None, window, cx);
this.activate_item(0, false, false, window, cx);
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx);
+ let rightmost_terminal = SubView::new(
+ debug_terminal.focus_handle(cx),
+ debug_terminal.clone().into(),
+ DebuggerPaneItem::Terminal,
+ running_state,
+ rightmost_pane.downgrade(),
+ cx,
+ );
rightmost_pane.update(cx, |this, cx| {
- this.add_item(
- Box::new(SubView::new(
- debug_terminal.focus_handle(cx),
- debug_terminal.clone().into(),
- DebuggerPaneItem::Terminal,
- cx,
- )),
- false,
- false,
- None,
- window,
- cx,
- );
+ this.add_item(Box::new(rightmost_terminal), false, false, None, window, cx);
});
subscriptions.extend(
@@ -415,6 +415,8 @@ impl TerminalBuilder {
event_loop_task: Task::ready(Ok(())),
background_executor: background_executor.clone(),
path_style,
+ #[cfg(any(test, feature = "test-support"))]
+ input_log: Vec::new(),
};
Ok(TerminalBuilder {
@@ -646,6 +648,8 @@ impl TerminalBuilder {
event_loop_task: Task::ready(Ok(())),
background_executor,
path_style,
+ #[cfg(any(test, feature = "test-support"))]
+ input_log: Vec::new(),
};
if !activation_script.is_empty() && no_task {
@@ -870,6 +874,8 @@ pub struct Terminal {
event_loop_task: Task<Result<(), anyhow::Error>>,
background_executor: BackgroundExecutor,
path_style: PathStyle,
+ #[cfg(any(test, feature = "test-support"))]
+ input_log: Vec<Vec<u8>>,
}
struct CopyTemplate {
@@ -1451,9 +1457,18 @@ impl Terminal {
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
self.events.push_back(InternalEvent::SetSelection(None));
+ let input = input.into();
+ #[cfg(any(test, feature = "test-support"))]
+ self.input_log.push(input.to_vec());
+
self.write_to_pty(input);
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn take_input_log(&mut self) -> Vec<Vec<u8>> {
+ std::mem::take(&mut self.input_log)
+ }
+
pub fn toggle_vi_mode(&mut self) {
self.events.push_back(InternalEvent::ToggleViMode);
}
@@ -53,6 +53,7 @@ editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
+terminal = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[package.metadata.cargo-machete]
@@ -1,4 +1,4 @@
-use std::{cmp, ops::ControlFlow, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration};
+use std::{cmp, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration};
use crate::{
TerminalView, default_working_directory,
@@ -12,11 +12,11 @@ use db::kvp::KEY_VALUE_STORE;
use futures::{channel::oneshot, future::join_all};
use gpui::{
Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter,
- ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled,
- Task, WeakEntity, Window, actions,
+ FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, Task, WeakEntity,
+ Window, actions,
};
use itertools::Itertools;
-use project::{Fs, Project, ProjectEntryId};
+use project::{Fs, Project};
use settings::{Settings, TerminalDockPosition};
use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId};
@@ -28,13 +28,13 @@ use ui::{
use util::{ResultExt, TryFutureExt};
use workspace::{
ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
- ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
+ ActivatePaneUp, ActivatePreviousPane, DraggedTab, ItemId, MoveItemToPane,
MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, Pane,
PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp, SwapPaneDown,
SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
item::SerializableItem,
- move_active_item, move_item, pane,
+ move_active_item, pane,
};
use anyhow::{Result, anyhow};
@@ -133,7 +133,11 @@ impl TerminalPanel {
}
}
- fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {
+ pub(crate) fn apply_tab_bar_buttons(
+ &self,
+ terminal_pane: &Entity<Pane>,
+ cx: &mut Context<Self>,
+ ) {
let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
terminal_pane.update(cx, |pane, cx| {
pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {
@@ -1187,7 +1191,6 @@ pub fn new_terminal_pane(
window: &mut Window,
cx: &mut Context<TerminalPanel>,
) -> Entity<Pane> {
- let is_local = project.read(cx).is_local();
let terminal_panel = cx.entity();
let pane = cx.new(|cx| {
let mut pane = Pane::new(
@@ -1245,113 +1248,6 @@ pub fn new_terminal_pane(
toolbar.add_item(breadcrumbs, window, cx);
});
- let drop_closure_project = project.downgrade();
- let drop_closure_terminal_panel = terminal_panel.downgrade();
- pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| {
- let Some(project) = drop_closure_project.upgrade() else {
- return ControlFlow::Break(());
- };
- if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
- let this_pane = cx.entity();
- let item = if tab.pane == this_pane {
- pane.item_for_index(tab.ix)
- } else {
- tab.pane.read(cx).item_for_index(tab.ix)
- };
- if let Some(item) = item {
- if item.downcast::<TerminalView>().is_some() {
- let source = tab.pane.clone();
- let item_id_to_move = item.item_id();
-
- // If no split direction, let the regular pane drop handler take care of it
- let Some(split_direction) = pane.drag_split_direction() else {
- return ControlFlow::Continue(());
- };
-
- // Gather data synchronously before deferring
- let is_zoomed = drop_closure_terminal_panel
- .upgrade()
- .map(|terminal_panel| {
- let terminal_panel = terminal_panel.read(cx);
- if terminal_panel.active_pane == this_pane {
- pane.is_zoomed()
- } else {
- terminal_panel.active_pane.read(cx).is_zoomed()
- }
- })
- .unwrap_or(false);
-
- let workspace = workspace.clone();
- let terminal_panel = drop_closure_terminal_panel.clone();
-
- // Defer the split operation to avoid re-entrancy panic.
- // The pane may be the one currently being updated, so we cannot
- // call mark_positions (via split) synchronously.
- cx.spawn_in(window, async move |_, cx| {
- cx.update(|window, cx| {
- let Ok(new_pane) =
- terminal_panel.update(cx, |terminal_panel, cx| {
- let new_pane = new_terminal_pane(
- workspace, project, is_zoomed, window, cx,
- );
- terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
- terminal_panel.center.split(
- &this_pane,
- &new_pane,
- split_direction,
- cx,
- );
- new_pane
- })
- else {
- return;
- };
-
- move_item(
- &source,
- &new_pane,
- item_id_to_move,
- new_pane.read(cx).active_item_index(),
- true,
- window,
- cx,
- );
- })
- .ok();
- })
- .detach();
- } else if let Some(project_path) = item.project_path(cx)
- && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
- {
- add_paths_to_terminal(pane, &[entry_path], window, cx);
- }
- }
- } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
- let project = project.read(cx);
- let paths_to_add = selection
- .items()
- .map(|selected_entry| selected_entry.entry_id)
- .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
- .filter_map(|project_path| project.absolute_path(&project_path, cx))
- .collect::<Vec<_>>();
- if !paths_to_add.is_empty() {
- add_paths_to_terminal(pane, &paths_to_add, window, cx);
- }
- } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
- if let Some(entry_path) = project
- .read(cx)
- .path_for_entry(entry_id, cx)
- .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx))
- {
- add_paths_to_terminal(pane, &[entry_path], window, cx);
- }
- } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
- add_paths_to_terminal(pane, paths.paths(), window, cx);
- }
-
- ControlFlow::Break(())
- });
-
pane
});
@@ -1376,27 +1272,6 @@ async fn wait_for_terminals_tasks(
join_all(pending_tasks).await;
}
-fn add_paths_to_terminal(
- pane: &mut Pane,
- paths: &[PathBuf],
- window: &mut Window,
- cx: &mut Context<Pane>,
-) {
- if let Some(terminal_view) = pane
- .active_item()
- .and_then(|item| item.downcast::<TerminalView>())
- {
- window.focus(&terminal_view.focus_handle(cx), cx);
- let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
- new_text.push(' ');
- terminal_view.update(cx, |terminal_view, cx| {
- terminal_view.terminal().update(cx, |terminal, _| {
- terminal.paste(&new_text);
- });
- });
- }
-}
-
struct FailedToSpawnTerminal {
error: String,
focus_handle: FocusHandle,
@@ -8,18 +8,20 @@ mod terminal_slash_command;
use assistant_slash_command::SlashCommandRegistry;
use editor::{Editor, EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
use gpui::{
- Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
- Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Point,
- Render, ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred,
- div,
+ Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, ExternalPaths,
+ FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent,
+ Pixels, Point, Render, ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions,
+ anchored, deferred, div,
};
+use itertools::Itertools;
use menu;
use persistence::TERMINAL_DB;
-use project::{Project, search::SearchQuery};
+use project::{Project, ProjectEntryId, search::SearchQuery};
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory};
use std::{
+ any::Any,
cmp,
ops::{Range, RangeInclusive},
path::{Path, PathBuf},
@@ -50,8 +52,8 @@ use ui::{
};
use util::ResultExt;
use workspace::{
- CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId,
- delete_unloaded_items,
+ CloseActiveItem, DraggedSelection, DraggedTab, NewCenterTerminal, NewTerminal, Pane,
+ ToolbarItemLocation, Workspace, WorkspaceId, delete_unloaded_items,
item::{
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
},
@@ -833,6 +835,15 @@ impl TerminalView {
});
}
+ fn add_paths_to_terminal(&self, paths: &[PathBuf], window: &mut Window, cx: &mut App) {
+ let mut text = paths.iter().map(|path| format!(" {path:?}")).join("");
+ text.push(' ');
+ window.focus(&self.focus_handle(cx), cx);
+ self.terminal.update(cx, |terminal, _| {
+ terminal.paste(&text);
+ });
+ }
+
fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
self.clear_bell(cx);
self.terminal.update(cx, |term, _| {
@@ -1412,6 +1423,154 @@ impl Item for TerminalView {
None
}
+ fn handle_drop(
+ &self,
+ active_pane: &Pane,
+ dropped: &dyn Any,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> bool {
+ let Some(project) = self.project.upgrade() else {
+ return false;
+ };
+
+ if let Some(paths) = dropped.downcast_ref::<ExternalPaths>() {
+ let is_local = project.read(cx).is_local();
+ if is_local {
+ self.add_paths_to_terminal(paths.paths(), window, cx);
+ return true;
+ }
+
+ return false;
+ } else if let Some(tab) = dropped.downcast_ref::<DraggedTab>() {
+ let Some(self_handle) = self.self_handle.upgrade() else {
+ return false;
+ };
+
+ let Some(workspace) = self.workspace.upgrade() else {
+ return false;
+ };
+
+ let Some(this_pane) = workspace.read(cx).pane_for(&self_handle) else {
+ return false;
+ };
+
+ let item = if tab.pane == this_pane {
+ active_pane.item_for_index(tab.ix)
+ } else {
+ tab.pane.read(cx).item_for_index(tab.ix)
+ };
+
+ let Some(item) = item else {
+ return false;
+ };
+
+ if item.downcast::<TerminalView>().is_some() {
+ let Some(split_direction) = active_pane.drag_split_direction() else {
+ return false;
+ };
+
+ let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
+ return false;
+ };
+
+ if !terminal_panel.read(cx).center.panes().contains(&&this_pane) {
+ return false;
+ }
+
+ let source = tab.pane.clone();
+ let item_id_to_move = item.item_id();
+ let is_zoomed = {
+ let terminal_panel = terminal_panel.read(cx);
+ if terminal_panel.active_pane == this_pane {
+ active_pane.is_zoomed()
+ } else {
+ terminal_panel.active_pane.read(cx).is_zoomed()
+ }
+ };
+
+ let workspace = workspace.downgrade();
+ let terminal_panel = terminal_panel.downgrade();
+ // Defer the split operation to avoid re-entrancy panic.
+ // The pane may be the one currently being updated, so we cannot
+ // call mark_positions (via split) synchronously.
+ window
+ .spawn(cx, async move |cx| {
+ cx.update(|window, cx| {
+ let Ok(new_pane) = terminal_panel.update(cx, |terminal_panel, cx| {
+ let new_pane = terminal_panel::new_terminal_pane(
+ workspace, project, is_zoomed, window, cx,
+ );
+ terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
+ terminal_panel.center.split(
+ &this_pane,
+ &new_pane,
+ split_direction,
+ cx,
+ );
+ anyhow::Ok(new_pane)
+ }) else {
+ return;
+ };
+
+ let Some(new_pane) = new_pane.log_err() else {
+ return;
+ };
+
+ workspace::move_item(
+ &source,
+ &new_pane,
+ item_id_to_move,
+ new_pane.read(cx).active_item_index(),
+ true,
+ window,
+ cx,
+ );
+ })
+ .ok();
+ })
+ .detach();
+
+ return true;
+ } else {
+ if let Some(project_path) = item.project_path(cx)
+ && let Some(path) = project.read(cx).absolute_path(&project_path, cx)
+ {
+ self.add_paths_to_terminal(&[path], window, cx);
+ return true;
+ }
+ }
+
+ return false;
+ } else if let Some(selection) = dropped.downcast_ref::<DraggedSelection>() {
+ let project = project.read(cx);
+ let paths = selection
+ .items()
+ .map(|selected_entry| selected_entry.entry_id)
+ .filter_map(|entry_id| project.path_for_entry(entry_id, cx))
+ .filter_map(|project_path| project.absolute_path(&project_path, cx))
+ .collect::<Vec<_>>();
+
+ if !paths.is_empty() {
+ self.add_paths_to_terminal(&paths, window, cx);
+ }
+
+ return true;
+ } else if let Some(&entry_id) = dropped.downcast_ref::<ProjectEntryId>() {
+ let project = project.read(cx);
+ if let Some(path) = project
+ .path_for_entry(entry_id, cx)
+ .and_then(|project_path| project.absolute_path(&project_path, cx))
+ {
+ self.add_paths_to_terminal(&[path], window, cx);
+ }
+
+ return true;
+ }
+
+ false
+ }
+
fn tab_extra_context_menu_actions(
&self,
_window: &mut Window,
@@ -1840,10 +1999,46 @@ mod tests {
use super::*;
use gpui::TestAppContext;
use project::{Entry, Project, ProjectPath, Worktree};
- use std::path::Path;
+ use std::path::{Path, PathBuf};
use util::paths::PathStyle;
use util::rel_path::RelPath;
- use workspace::{AppState, MultiWorkspace};
+ use workspace::item::test::{TestItem, TestProjectItem};
+ use workspace::{AppState, MultiWorkspace, SelectedEntry};
+
+ fn expected_drop_text(paths: &[PathBuf]) -> String {
+ let mut text = String::new();
+ for path in paths {
+ text.push(' ');
+ text.push_str(&format!("{path:?}"));
+ }
+ text.push(' ');
+ text
+ }
+
+ fn assert_drop_writes_to_terminal(
+ pane: &Entity<Pane>,
+ terminal_view_index: usize,
+ terminal: &Entity<Terminal>,
+ dropped: &dyn Any,
+ expected_text: &str,
+ window: &mut Window,
+ cx: &mut Context<MultiWorkspace>,
+ ) {
+ let _ = terminal.update(cx, |terminal, _| terminal.take_input_log());
+
+ let handled = pane.update(cx, |pane, cx| {
+ pane.item_for_index(terminal_view_index)
+ .unwrap()
+ .handle_drop(pane, dropped, window, cx)
+ });
+ assert!(handled, "handle_drop should return true for {:?}", dropped);
+
+ let mut input_log = terminal.update(cx, |terminal, _| terminal.take_input_log());
+ assert_eq!(input_log.len(), 1, "expected exactly one write to terminal");
+ let written =
+ String::from_utf8(input_log.remove(0)).expect("terminal write should be valid UTF-8");
+ assert_eq!(written, expected_text);
+ }
// Working directory calculation tests
@@ -1972,24 +2167,7 @@ mod tests {
let (project, _workspace) = init_test(cx).await;
let (wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
- let entry = cx
- .update(|cx| {
- wt.update(cx, |wt, cx| {
- wt.create_entry(
- RelPath::new(Path::new("src/main.rs"), PathStyle::local())
- .unwrap()
- .as_ref()
- .into(),
- false,
- None,
- cx,
- )
- })
- })
- .await
- .unwrap()
- .into_included()
- .unwrap();
+ let entry = create_file_in_worktree(wt.clone(), "src/main.rs", cx).await;
insert_active_entry_for(wt, entry, project.clone(), cx);
cx.update(|cx| {
@@ -2014,6 +2192,18 @@ mod tests {
/// Creates a worktree with 1 file: /root.txt
pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
+ let (project, workspace, _) = init_test_with_window(cx).await;
+ (project, workspace)
+ }
+
+ /// Creates a worktree with 1 file /root.txt and returns the project, workspace, and window handle.
+ async fn init_test_with_window(
+ cx: &mut TestAppContext,
+ ) -> (
+ Entity<Project>,
+ Entity<Workspace>,
+ gpui::WindowHandle<MultiWorkspace>,
+ ) {
let params = cx.update(AppState::test);
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
@@ -2026,7 +2216,32 @@ mod tests {
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
- (project, workspace)
+ (project, workspace, window_handle)
+ }
+
+ /// Creates a file in the given worktree and returns its entry.
+ async fn create_file_in_worktree(
+ worktree: Entity<Worktree>,
+ relative_path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) -> Entry {
+ cx.update(|cx| {
+ worktree.update(cx, |worktree, cx| {
+ worktree.create_entry(
+ RelPath::new(relative_path.as_ref(), PathStyle::local())
+ .unwrap()
+ .as_ref()
+ .into(),
+ false,
+ None,
+ cx,
+ )
+ })
+ })
+ .await
+ .unwrap()
+ .into_included()
+ .unwrap()
}
/// Creates a worktree with 1 folder: /root{suffix}/
@@ -2089,6 +2304,183 @@ mod tests {
});
}
+ // Terminal drag/drop test
+
+ #[gpui::test]
+ async fn test_handle_drop_writes_paths_for_all_drop_types(cx: &mut TestAppContext) {
+ let (project, _workspace, window_handle) = init_test_with_window(cx).await;
+
+ let (worktree, _) = create_folder_wt(project.clone(), "/root/", cx).await;
+ let first_entry = create_file_in_worktree(worktree.clone(), "first.txt", cx).await;
+ let second_entry = create_file_in_worktree(worktree.clone(), "second.txt", cx).await;
+
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+ let first_path = project
+ .read_with(cx, |project, cx| {
+ project.absolute_path(
+ &ProjectPath {
+ worktree_id,
+ path: first_entry.path.clone(),
+ },
+ cx,
+ )
+ })
+ .unwrap();
+ let second_path = project
+ .read_with(cx, |project, cx| {
+ project.absolute_path(
+ &ProjectPath {
+ worktree_id,
+ path: second_entry.path.clone(),
+ },
+ cx,
+ )
+ })
+ .unwrap();
+
+ let (active_pane, terminal, terminal_view, tab_item) = window_handle
+ .update(cx, |multi_workspace, window, cx| {
+ let workspace = multi_workspace.workspace().clone();
+ let active_pane = workspace.read(cx).active_pane().clone();
+
+ let terminal = cx.new(|cx| {
+ terminal::TerminalBuilder::new_display_only(
+ CursorShape::default(),
+ terminal::terminal_settings::AlternateScroll::On,
+ None,
+ 0,
+ cx.background_executor(),
+ PathStyle::local(),
+ )
+ .unwrap()
+ .subscribe(cx)
+ });
+ let terminal_view = cx.new(|cx| {
+ TerminalView::new(
+ terminal.clone(),
+ workspace.downgrade(),
+ None,
+ project.downgrade(),
+ window,
+ cx,
+ )
+ });
+
+ active_pane.update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(terminal_view.clone()),
+ true,
+ false,
+ None,
+ window,
+ cx,
+ );
+ });
+
+ let tab_project_item = cx.new(|_| TestProjectItem {
+ entry_id: Some(second_entry.id),
+ project_path: Some(ProjectPath {
+ worktree_id,
+ path: second_entry.path.clone(),
+ }),
+ is_dirty: false,
+ });
+ let tab_item =
+ cx.new(|cx| TestItem::new(cx).with_project_items(&[tab_project_item]));
+ active_pane.update(cx, |pane, cx| {
+ pane.add_item(Box::new(tab_item.clone()), true, false, None, window, cx);
+ });
+
+ (active_pane, terminal, terminal_view, tab_item)
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ window_handle
+ .update(cx, |multi_workspace, window, cx| {
+ let workspace = multi_workspace.workspace().clone();
+ let terminal_view_index =
+ active_pane.read(cx).index_for_item(&terminal_view).unwrap();
+ let dragged_tab_index = active_pane.read(cx).index_for_item(&tab_item).unwrap();
+
+ assert!(
+ workspace.read(cx).pane_for(&terminal_view).is_some(),
+ "terminal view not registered with workspace after run_until_parked"
+ );
+
+ // Dragging an external file should write its path to the terminal
+ let external_paths = ExternalPaths(vec![first_path.clone()].into());
+ assert_drop_writes_to_terminal(
+ &active_pane,
+ terminal_view_index,
+ &terminal,
+ &external_paths,
+ &expected_drop_text(std::slice::from_ref(&first_path)),
+ window,
+ cx,
+ );
+
+ // Dragging a tab should write the path of the tab's item to the terminal
+ let dragged_tab = DraggedTab {
+ pane: active_pane.clone(),
+ item: Box::new(tab_item.clone()),
+ ix: dragged_tab_index,
+ detail: 0,
+ is_active: false,
+ };
+ assert_drop_writes_to_terminal(
+ &active_pane,
+ terminal_view_index,
+ &terminal,
+ &dragged_tab,
+ &expected_drop_text(std::slice::from_ref(&second_path)),
+ window,
+ cx,
+ );
+
+ // Dragging multiple selections should write both paths to the terminal
+ let dragged_selection = DraggedSelection {
+ active_selection: SelectedEntry {
+ worktree_id,
+ entry_id: first_entry.id,
+ },
+ marked_selections: Arc::from([
+ SelectedEntry {
+ worktree_id,
+ entry_id: first_entry.id,
+ },
+ SelectedEntry {
+ worktree_id,
+ entry_id: second_entry.id,
+ },
+ ]),
+ };
+ assert_drop_writes_to_terminal(
+ &active_pane,
+ terminal_view_index,
+ &terminal,
+ &dragged_selection,
+ &expected_drop_text(&[first_path.clone(), second_path.clone()]),
+ window,
+ cx,
+ );
+
+ // Dropping a project entry should write the entry's path to the terminal
+ let dropped_entry_id = first_entry.id;
+ assert_drop_writes_to_terminal(
+ &active_pane,
+ terminal_view_index,
+ &terminal,
+ &dropped_entry_id,
+ &expected_drop_text(&[first_path]),
+ window,
+ cx,
+ );
+ })
+ .unwrap();
+ }
+
// Terminal rename tests
#[gpui::test]
@@ -366,6 +366,18 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
true
}
+ /// Called when the containing pane receives a drop on the item or the item's tab.
+ /// Returns `true` to consume it and suppress the pane's default drop behavior.
+ fn handle_drop(
+ &self,
+ _active_pane: &Pane,
+ _dropped: &dyn Any,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> bool {
+ false
+ }
+
/// Returns additional actions to add to the tab's context menu.
/// Each entry is a label and an action to dispatch.
fn tab_extra_context_menu_actions(
@@ -545,6 +557,13 @@ pub trait ItemHandle: 'static + Send {
fn preserve_preview(&self, cx: &App) -> bool;
fn include_in_nav_history(&self) -> bool;
fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
+ fn handle_drop(
+ &self,
+ active_pane: &Pane,
+ dropped: &dyn Any,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> bool;
fn tab_extra_context_menu_actions(
&self,
window: &mut Window,
@@ -1110,6 +1129,20 @@ impl<T: Item> ItemHandle for Entity<T> {
})
}
+ /// Called when the containing pane receives a drop on the item or the item's tab.
+ /// Returns `true` if the item handled it and the pane should skip its default drop behavior.
+ fn handle_drop(
+ &self,
+ active_pane: &Pane,
+ dropped: &dyn Any,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> bool {
+ self.update(cx, |this, cx| {
+ this.handle_drop(active_pane, dropped, window, cx)
+ })
+ }
+
fn tab_extra_context_menu_actions(
&self,
window: &mut Window,
@@ -34,7 +34,6 @@ use std::{
any::Any,
cmp, fmt, mem,
num::NonZeroUsize,
- ops::ControlFlow,
path::PathBuf,
rc::Rc,
sync::{
@@ -382,9 +381,6 @@ pub struct Pane {
project: WeakEntity<Project>,
pub drag_split_direction: Option<SplitDirection>,
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool>>,
- custom_drop_handle: Option<
- Arc<dyn Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>>,
- >,
can_split_predicate:
Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut Window, &mut Context<Self>) -> bool>>,
can_toggle_zoom: bool,
@@ -567,7 +563,6 @@ impl Pane {
workspace,
project: project.downgrade(),
can_drop_predicate,
- custom_drop_handle: None,
can_split_predicate: None,
can_toggle_zoom: true,
should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show),
@@ -846,15 +841,6 @@ impl Pane {
cx.notify();
}
- pub fn set_custom_drop_handle<F>(&mut self, cx: &mut Context<Self>, handle: F)
- where
- F: 'static
- + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context<Pane>) -> ControlFlow<(), ()>,
- {
- self.custom_drop_handle = Some(Arc::new(handle));
- cx.notify();
- }
-
pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
ItemNavHistory {
history: self.nav_history.clone(),
@@ -2901,7 +2887,7 @@ impl Pane {
.on_drop(
cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, ix, window, cx)
+ this.handle_tab_drop(dragged_tab, ix, false, window, cx)
}),
)
.on_drop(
@@ -3550,7 +3536,7 @@ impl Pane {
.on_drop(
cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
this.drag_split_direction = None;
- this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
+ this.handle_tab_drop(dragged_tab, this.items.len(), false, window, cx)
}),
)
.on_drop(
@@ -3691,14 +3677,18 @@ impl Pane {
&mut self,
dragged_tab: &DraggedTab,
ix: usize,
+ is_pane_target: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(custom_drop_handle) = self.custom_drop_handle.clone()
- && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx)
+ if is_pane_target
+ && ix == self.active_item_index
+ && let Some(active_item) = self.active_item()
+ && active_item.handle_drop(self, dragged_tab, window, cx)
{
return;
}
+
let mut to_pane = cx.entity();
let split_direction = self.drag_split_direction;
let item_id = dragged_tab.item.item_id();
@@ -3791,7 +3781,7 @@ impl Pane {
let item_id = dragged_tab.item.item_id();
let pinned_count = self.pinned_tab_count;
- self.handle_tab_drop(dragged_tab, pinned_count, window, cx);
+ self.handle_tab_drop(dragged_tab, pinned_count, false, window, cx);
let to_pane = cx.entity();
@@ -3843,11 +3833,12 @@ impl Pane {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(custom_drop_handle) = self.custom_drop_handle.clone()
- && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx)
+ if let Some(active_item) = self.active_item()
+ && active_item.handle_drop(self, dragged_selection, window, cx)
{
return;
}
+
self.handle_project_entry_drop(
&dragged_selection.active_selection.entry_id,
dragged_onto,
@@ -3863,11 +3854,12 @@ impl Pane {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(custom_drop_handle) = self.custom_drop_handle.clone()
- && let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx)
+ if let Some(active_item) = self.active_item()
+ && active_item.handle_drop(self, project_entry_id, window, cx)
{
return;
}
+
let mut to_pane = cx.entity();
let split_direction = self.drag_split_direction;
let project_entry_id = *project_entry_id;
@@ -3939,11 +3931,12 @@ impl Pane {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(custom_drop_handle) = self.custom_drop_handle.clone()
- && let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx)
+ if let Some(active_item) = self.active_item()
+ && active_item.handle_drop(self, paths, window, cx)
{
return;
}
+
let mut to_pane = cx.entity();
let mut split_direction = self.drag_split_direction;
let paths = paths.paths().to_vec();
@@ -4424,6 +4417,7 @@ impl Render for Pane {
this.handle_tab_drop(
dragged_tab,
this.active_item_index(),
+ true,
window,
cx,
)
@@ -4826,7 +4820,7 @@ impl Render for DraggedTab {
#[cfg(test)]
mod tests {
- use std::{iter::zip, num::NonZero};
+ use std::{cell::Cell, iter::zip, num::NonZero};
use super::*;
use crate::{
@@ -4839,6 +4833,65 @@ mod tests {
use theme::LoadThemes;
use util::TryFutureExt;
+ // drop_call_count is a Cell here because `handle_drop` takes &self, not &mut self.
+ struct CustomDropHandlingItem {
+ focus_handle: gpui::FocusHandle,
+ drop_call_count: Cell<usize>,
+ }
+
+ impl CustomDropHandlingItem {
+ fn new(cx: &mut Context<Self>) -> Self {
+ Self {
+ focus_handle: cx.focus_handle(),
+ drop_call_count: Cell::new(0),
+ }
+ }
+
+ fn drop_call_count(&self) -> usize {
+ self.drop_call_count.get()
+ }
+ }
+
+ impl EventEmitter<()> for CustomDropHandlingItem {}
+
+ impl Focusable for CustomDropHandlingItem {
+ fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+ }
+
+ impl Render for CustomDropHandlingItem {
+ fn render(
+ &mut self,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) -> impl gpui::IntoElement {
+ gpui::Empty
+ }
+ }
+
+ impl Item for CustomDropHandlingItem {
+ type Event = ();
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> gpui::SharedString {
+ "custom_drop_handling_item".into()
+ }
+
+ fn handle_drop(
+ &self,
+ _active_pane: &Pane,
+ dropped: &dyn std::any::Any,
+ _window: &mut Window,
+ _cx: &mut App,
+ ) -> bool {
+ let is_dragged_tab = dropped.downcast_ref::<DraggedTab>().is_some();
+ if is_dragged_tab {
+ self.drop_call_count.set(self.drop_call_count.get() + 1);
+ }
+ is_dragged_tab
+ }
+ }
+
#[gpui::test]
async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
init_test(cx);
@@ -5664,6 +5717,83 @@ mod tests {
assert_item_labels(&pane, ["C", "A", "B*"], cx);
}
+ #[gpui::test]
+ async fn test_handle_tab_drop_respects_is_pane_target(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 source_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+ let item_a = add_labeled_item(&source_pane, "A", false, cx);
+ let item_b = add_labeled_item(&source_pane, "B", false, cx);
+
+ let target_pane = workspace.update_in(cx, |workspace, window, cx| {
+ workspace.split_pane(source_pane.clone(), SplitDirection::Right, window, cx)
+ });
+
+ let custom_item = target_pane.update_in(cx, |pane, window, cx| {
+ let custom_item = Box::new(cx.new(CustomDropHandlingItem::new));
+ pane.add_item(custom_item.clone(), true, true, None, window, cx);
+ custom_item
+ });
+
+ let moved_item_id = item_a.item_id();
+ let other_item_id = item_b.item_id();
+ let custom_item_id = custom_item.item_id();
+
+ let pane_item_ids = |pane: &Entity<Pane>, cx: &mut VisualTestContext| {
+ pane.read_with(cx, |pane, _| {
+ pane.items().map(|item| item.item_id()).collect::<Vec<_>>()
+ })
+ };
+
+ let source_before_item_ids = pane_item_ids(&source_pane, cx);
+ assert_eq!(source_before_item_ids, vec![moved_item_id, other_item_id]);
+
+ let target_before_item_ids = pane_item_ids(&target_pane, cx);
+ assert_eq!(target_before_item_ids, vec![custom_item_id]);
+
+ let dragged_tab = DraggedTab {
+ pane: source_pane.clone(),
+ item: item_a.boxed_clone(),
+ ix: 0,
+ detail: 0,
+ is_active: true,
+ };
+
+ // Dropping item_a onto the target pane itself means the
+ // custom item handles the drop and no tab move should occur
+ target_pane.update_in(cx, |pane, window, cx| {
+ pane.handle_tab_drop(&dragged_tab, pane.active_item_index(), true, window, cx);
+ });
+ cx.run_until_parked();
+
+ assert_eq!(
+ custom_item.read_with(cx, |item, _| item.drop_call_count()),
+ 1
+ );
+ assert_eq!(pane_item_ids(&source_pane, cx), source_before_item_ids);
+ assert_eq!(pane_item_ids(&target_pane, cx), target_before_item_ids);
+
+ // Dropping item_a onto the tab target means the custom handler
+ // should be skipped and the pane's default tab drop behavior should run.
+ target_pane.update_in(cx, |pane, window, cx| {
+ pane.handle_tab_drop(&dragged_tab, pane.active_item_index(), false, window, cx);
+ });
+ cx.run_until_parked();
+
+ assert_eq!(
+ custom_item.read_with(cx, |item, _| item.drop_call_count()),
+ 1
+ );
+ assert_eq!(pane_item_ids(&source_pane, cx), vec![other_item_id]);
+
+ let target_item_ids = pane_item_ids(&target_pane, cx);
+ assert_eq!(target_item_ids, vec![moved_item_id, custom_item_id]);
+ }
+
#[gpui::test]
async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab(
cx: &mut TestAppContext,
@@ -5699,7 +5829,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, true, window, cx);
});
// A should be moved to new pane. B should remain pinned, A should not be pinned
@@ -5748,7 +5878,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, true, window, cx);
});
// A should be moved to new pane. Both A and B should still be pinned
@@ -5798,7 +5928,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// A should stay pinned
@@ -5846,7 +5976,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// A should become pinned
@@ -5890,7 +6020,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// A should stay pinned
@@ -5952,7 +6082,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// E (unpinned) should be closed, leaving 3 pinned items
@@ -5987,7 +6117,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// A should still be pinned and active
@@ -6027,7 +6157,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 2, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
});
// A stays pinned
@@ -6064,7 +6194,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// Neither are pinned
@@ -6101,7 +6231,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 2, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
});
// A becomes unpinned
@@ -6138,7 +6268,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// A becomes unpinned
@@ -6174,7 +6304,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// A stays pinned, B and C remain unpinned
@@ -6215,7 +6345,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// A should become pinned since it was dropped in the pinned region
@@ -6257,7 +6387,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, true, window, cx);
});
// A should remain unpinned since it was dropped outside the pinned region
@@ -6304,7 +6434,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// A should be after B and all are pinned
@@ -6319,7 +6449,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 2, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
});
// A should be after C and all are pinned
@@ -6334,7 +6464,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 1, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 1, false, window, cx);
});
// A should be before C and all are pinned
@@ -6349,7 +6479,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// A should be before B and all are pinned
@@ -6381,7 +6511,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 2, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 2, false, window, cx);
});
// A should be at the end
@@ -6413,7 +6543,7 @@ mod tests {
detail: 0,
is_active: true,
};
- pane.handle_tab_drop(&dragged_tab, 0, window, cx);
+ pane.handle_tab_drop(&dragged_tab, 0, false, window, cx);
});
// C should be at the beginning