diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 336783212f101c048ea0693edbb01a0a2089fbbd..489866907fbf640d3888663717db22fb3bda8ec1 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -12,8 +12,9 @@ use dap::{ }; use futures::{SinkExt as _, channel::mpsc}; use gpui::{ - Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Subscription, Task, WeakEntity, actions, + Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, + FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, + actions, anchored, deferred, }; use project::{ @@ -64,6 +65,7 @@ pub struct DebugPanel { project: WeakEntity, workspace: WeakEntity, focus_handle: FocusHandle, + context_menu: Option<(Entity, Point, Subscription)>, _subscriptions: Vec, } @@ -126,6 +128,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), project: project.downgrade(), workspace: workspace.weak_handle(), + context_menu: None, }; debug_panel @@ -573,6 +576,57 @@ impl DebugPanel { ) } + fn deploy_context_menu( + &mut self, + position: Point, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(running_state) = self + .active_session + .as_ref() + .and_then(|session| session.read(cx).mode().as_running().cloned()) + { + let pane_items_status = running_state.read(cx).pane_items_status(cx); + let this = cx.weak_entity(); + + let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| { + for (item_kind, is_visible) in pane_items_status.into_iter() { + menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, { + let this = this.clone(); + move |window, cx| { + this.update(cx, |this, cx| { + if let Some(running_state) = + this.active_session.as_ref().and_then(|session| { + session.read(cx).mode().as_running().cloned() + }) + { + running_state.update(cx, |state, cx| { + if is_visible { + state.remove_pane_item(item_kind, window, cx); + } else { + state.add_pane_item(item_kind, position, window, cx); + } + }) + } + }) + .ok(); + } + }); + } + + menu + }); + + window.focus(&context_menu.focus_handle(cx)); + let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); + } + } + fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ let active_session = self.active_session.clone(); @@ -897,11 +951,49 @@ impl Render for DebugPanel { let has_sessions = self.sessions.len() > 0; debug_assert_eq!(has_sessions, self.active_session.is_some()); + if self + .active_session + .as_ref() + .and_then(|session| session.read(cx).mode().as_running().cloned()) + .map(|state| state.read(cx).has_open_context_menu(cx)) + .unwrap_or(false) + { + self.context_menu.take(); + } + v_flex() .size_full() .key_context("DebugPanel") .child(h_flex().children(self.top_controls_strip(window, cx))) .track_focus(&self.focus_handle(cx)) + .when(self.active_session.is_some(), |this| { + this.on_mouse_down( + MouseButton::Right, + cx.listener(|this, event: &MouseDownEvent, window, cx| { + if this + .active_session + .as_ref() + .and_then(|session| { + session.read(cx).mode().as_running().map(|state| { + state.read(cx).has_pane_at_position(event.position) + }) + }) + .unwrap_or(false) + { + this.deploy_context_menu(event.position, window, cx); + } + }), + ) + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + deferred( + anchored() + .position(*position) + .anchor(gpui::Corner::TopLeft) + .child(menu.clone()), + ) + .with_priority(1) + })) + }) .map(|this| { if has_sessions { this.children(self.active_session.clone()) diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index f77a723adf326f057e52f1acfbe986d839175abd..283efd0d76ce78d5e27fac01c8f34c832c4801c3 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -1,4 +1,5 @@ use collections::HashMap; +use dap::Capabilities; use db::kvp::KEY_VALUE_STORE; use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window}; use project::Project; @@ -9,19 +10,43 @@ use workspace::{Member, Pane, PaneAxis, Workspace}; use crate::session::running::{ self, RunningState, SubView, breakpoint_list::BreakpointList, console::Console, - module_list::ModuleList, stack_frame_list::StackFrameList, variable_list::VariableList, + loaded_source_list::LoadedSourceList, module_list::ModuleList, + stack_frame_list::StackFrameList, variable_list::VariableList, }; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Hash, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub(crate) enum DebuggerPaneItem { Console, Variables, BreakpointList, Frames, Modules, + LoadedSources, } impl DebuggerPaneItem { + pub(crate) fn all() -> &'static [DebuggerPaneItem] { + static VARIANTS: &[DebuggerPaneItem] = &[ + DebuggerPaneItem::Console, + DebuggerPaneItem::Variables, + DebuggerPaneItem::BreakpointList, + DebuggerPaneItem::Frames, + DebuggerPaneItem::Modules, + DebuggerPaneItem::LoadedSources, + ]; + VARIANTS + } + + pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool { + match self { + DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(), + DebuggerPaneItem::LoadedSources => capabilities + .supports_loaded_sources_request + .unwrap_or_default(), + _ => true, + } + } + pub(crate) fn to_shared_string(self) -> SharedString { match self { DebuggerPaneItem::Console => SharedString::new_static("Console"), @@ -29,10 +54,17 @@ impl DebuggerPaneItem { DebuggerPaneItem::BreakpointList => SharedString::new_static("Breakpoints"), DebuggerPaneItem::Frames => SharedString::new_static("Frames"), DebuggerPaneItem::Modules => SharedString::new_static("Modules"), + DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"), } } } +impl From for SharedString { + fn from(item: DebuggerPaneItem) -> Self { + item.to_shared_string() + } +} + #[derive(Debug, Serialize, Deserialize)] pub(crate) struct SerializedAxis(pub Axis); @@ -136,6 +168,7 @@ pub(crate) fn deserialize_pane_layout( module_list: &Entity, console: &Entity, breakpoint_list: &Entity, + loaded_sources: &Entity, subscriptions: &mut HashMap, window: &mut Window, cx: &mut Context, @@ -157,6 +190,7 @@ pub(crate) fn deserialize_pane_layout( module_list, console, breakpoint_list, + loaded_sources, subscriptions, window, cx, @@ -191,7 +225,7 @@ pub(crate) fn deserialize_pane_layout( .iter() .map(|child| match child { DebuggerPaneItem::Frames => Box::new(SubView::new( - pane.focus_handle(cx), + stack_frame_list.focus_handle(cx), stack_frame_list.clone().into(), DebuggerPaneItem::Frames, None, @@ -212,13 +246,19 @@ pub(crate) fn deserialize_pane_layout( cx, )), DebuggerPaneItem::Modules => Box::new(SubView::new( - pane.focus_handle(cx), + module_list.focus_handle(cx), module_list.clone().into(), DebuggerPaneItem::Modules, None, cx, )), - + DebuggerPaneItem::LoadedSources => Box::new(SubView::new( + loaded_sources.focus_handle(cx), + loaded_sources.clone().into(), + DebuggerPaneItem::LoadedSources, + None, + cx, + )), DebuggerPaneItem::Console => Box::new(SubView::new( pane.focus_handle(cx), console.clone().into(), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 80ce42d2d603c10470718256e86cbf57f0183185..a099d70048f08c0ee81cc2758247c608d5ae2349 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -11,12 +11,12 @@ use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout}; use super::DebugPanelItemEvent; use breakpoint_list::BreakpointList; -use collections::HashMap; +use collections::{HashMap, IndexMap}; use console::Console; use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings}; use gpui::{ Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - NoAction, Subscription, Task, WeakEntity, + NoAction, Pixels, Point, Subscription, Task, WeakEntity, }; use loaded_source_list::LoadedSourceList; use module_list::ModuleList; @@ -49,8 +49,10 @@ pub struct RunningState { variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, - _module_list: Entity, + loaded_sources_list: Entity, + module_list: Entity, _console: Entity, + breakpoint_list: Entity, panes: PaneGroup, pane_close_subscriptions: HashMap, _schedule_serialize: Option>, @@ -383,7 +385,6 @@ impl RunningState { let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx)); - #[expect(unused)] let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); let console = cx.new(|cx| { @@ -396,7 +397,7 @@ impl RunningState { ) }); - let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx); + let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx); let _subscriptions = vec![ cx.observe(&module_list, |_, _, cx| cx.notify()), @@ -436,7 +437,8 @@ impl RunningState { &variable_list, &module_list, &console, - &breakpoints, + &breakpoint_list, + &loaded_source_list, &mut pane_close_subscriptions, window, cx, @@ -452,7 +454,7 @@ impl RunningState { &variable_list, &module_list, &console, - breakpoints, + &breakpoint_list, &mut pane_close_subscriptions, window, cx, @@ -472,13 +474,139 @@ impl RunningState { stack_frame_list, session_id, panes, - _module_list: module_list, + module_list, _console: console, + breakpoint_list, + loaded_sources_list: loaded_source_list, pane_close_subscriptions, _schedule_serialize: None, } } + pub(crate) fn remove_pane_item( + &mut self, + item_kind: DebuggerPaneItem, + window: &mut Window, + cx: &mut Context, + ) { + debug_assert!( + item_kind.is_supported(self.session.read(cx).capabilities()), + "We should only allow removing supported item kinds" + ); + + if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| { + Some(pane).zip( + pane.read(cx) + .items() + .find(|item| { + item.act_as::(cx) + .is_some_and(|view| view.read(cx).kind == item_kind) + }) + .map(|item| item.item_id()), + ) + }) { + pane.update(cx, |pane, cx| { + pane.remove_item(item_id, false, true, window, cx) + }) + } + } + + pub(crate) fn has_pane_at_position(&self, position: Point) -> bool { + self.panes.pane_at_pixel_position(position).is_some() + } + + pub(crate) fn add_pane_item( + &mut self, + item_kind: DebuggerPaneItem, + position: Point, + window: &mut Window, + cx: &mut Context, + ) { + debug_assert!( + item_kind.is_supported(self.session.read(cx).capabilities()), + "We should only allow adding supported item kinds" + ); + + if let Some(pane) = self.panes.pane_at_pixel_position(position) { + let sub_view = match item_kind { + DebuggerPaneItem::Console => { + let weak_console = self._console.clone().downgrade(); + + Box::new(SubView::new( + pane.focus_handle(cx), + self._console.clone().into(), + item_kind, + Some(Box::new(move |cx| { + weak_console + .read_with(cx, |console, cx| console.show_indicator(cx)) + .unwrap_or_default() + })), + cx, + )) + } + DebuggerPaneItem::Variables => Box::new(SubView::new( + self.variable_list.focus_handle(cx), + self.variable_list.clone().into(), + item_kind, + None, + cx, + )), + DebuggerPaneItem::BreakpointList => Box::new(SubView::new( + self.breakpoint_list.focus_handle(cx), + self.breakpoint_list.clone().into(), + item_kind, + None, + cx, + )), + DebuggerPaneItem::Frames => Box::new(SubView::new( + self.stack_frame_list.focus_handle(cx), + self.stack_frame_list.clone().into(), + item_kind, + None, + cx, + )), + DebuggerPaneItem::Modules => Box::new(SubView::new( + self.module_list.focus_handle(cx), + self.module_list.clone().into(), + item_kind, + None, + cx, + )), + DebuggerPaneItem::LoadedSources => Box::new(SubView::new( + self.loaded_sources_list.focus_handle(cx), + self.loaded_sources_list.clone().into(), + item_kind, + None, + cx, + )), + }; + + pane.update(cx, |pane, cx| { + pane.add_item(sub_view, false, false, None, window, cx); + }) + } + } + + pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap { + let caps = self.session.read(cx).capabilities(); + let mut pane_item_status = IndexMap::from_iter( + DebuggerPaneItem::all() + .iter() + .filter(|kind| kind.is_supported(&caps)) + .map(|kind| (*kind, false)), + ); + self.panes.panes().iter().for_each(|pane| { + pane.read(cx) + .items() + .filter_map(|item| item.act_as::(cx)) + .for_each(|view| { + pane_item_status.insert(view.read(cx).kind, true); + }); + }); + + pane_item_status + } + pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context) { if self._schedule_serialize.is_none() { self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| { @@ -533,6 +661,10 @@ impl RunningState { } } + pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool { + self.variable_list.read(cx).has_open_context_menu() + } + pub fn session(&self) -> &Entity { &self.session } @@ -557,7 +689,7 @@ impl RunningState { #[cfg(test)] pub(crate) fn module_list(&self) -> &Entity { - &self._module_list + &self.module_list } #[cfg(test)] @@ -793,7 +925,7 @@ impl RunningState { variable_list: &Entity, module_list: &Entity, console: &Entity, - breakpoints: Entity, + breakpoints: &Entity, subscriptions: &mut HashMap, window: &mut Window, cx: &mut Context<'_, RunningState>, @@ -817,7 +949,7 @@ impl RunningState { this.add_item( Box::new(SubView::new( breakpoints.focus_handle(cx), - breakpoints.into(), + breakpoints.clone().into(), DebuggerPaneItem::BreakpointList, None, cx, diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 0c4ad90a759a81a49679f060d3a3d050d4a5ec62..dd5487e0426ac8386a6af04d27e30d786bb29eaf 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -3,7 +3,7 @@ use project::debugger::session::{Session, SessionEvent}; use ui::prelude::*; use util::maybe; -pub struct LoadedSourceList { +pub(crate) struct LoadedSourceList { list: ListState, invalidate: bool, focus_handle: FocusHandle, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 05352b3b1737b6451629a5587ceddeb7b0a1fa65..75a041e1b20f12e74369ff0fd31fd5b89a35104d 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -194,6 +194,10 @@ impl VariableList { } } + pub(super) fn has_open_context_menu(&self) -> bool { + self.open_context_menu.is_some() + } + fn build_entries(&mut self, cx: &mut Context) { let Some(stack_frame_id) = self.selected_stack_frame_id else { return;