debugger: Add Debug Panel context menu (#28847)
This PR adds a debug panel context menu that will allow a user to select which debug session items are visible. The context menu will add to the pane that was right clicked on. <img width="1275" alt="Screenshot 2025-04-16 at 2 43 36 AM" src="https://github.com/user-attachments/assets/330322ff-69db-4731-bbaf-3544d53f2f15" /> Release Notes: - N/A
This commit is contained in:
parent
320abe9b22
commit
4efabe17dd
5 changed files with 287 additions and 19 deletions
|
@ -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<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -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<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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<Self>) -> Option<Div> {
|
||||
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())
|
||||
|
|
|
@ -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<DebuggerPaneItem> 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<ModuleList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoint_list: &Entity<BreakpointList>,
|
||||
loaded_sources: &Entity<LoadedSourceList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<RunningState>,
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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<variable_list::VariableList>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
stack_frame_list: Entity<stack_frame_list::StackFrameList>,
|
||||
_module_list: Entity<module_list::ModuleList>,
|
||||
loaded_sources_list: Entity<LoadedSourceList>,
|
||||
module_list: Entity<module_list::ModuleList>,
|
||||
_console: Entity<Console>,
|
||||
breakpoint_list: Entity<BreakpointList>,
|
||||
panes: PaneGroup,
|
||||
pane_close_subscriptions: HashMap<EntityId, Subscription>,
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
|
@ -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<Self>,
|
||||
) {
|
||||
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::<SubView>(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<Pixels>) -> bool {
|
||||
self.panes.pane_at_pixel_position(position).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn add_pane_item(
|
||||
&mut self,
|
||||
item_kind: DebuggerPaneItem,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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<DebuggerPaneItem, bool> {
|
||||
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::<SubView>(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<Self>) {
|
||||
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<Session> {
|
||||
&self.session
|
||||
}
|
||||
|
@ -557,7 +689,7 @@ impl RunningState {
|
|||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
|
||||
&self._module_list
|
||||
&self.module_list
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -793,7 +925,7 @@ impl RunningState {
|
|||
variable_list: &Entity<VariableList>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: Entity<BreakpointList>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Self>) {
|
||||
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||
return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue