debugger: Start on tabless design (#27837)

![image](https://github.com/user-attachments/assets/1cd54b70-5457-4c64-95bd-45a7055ea165)

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-04-03 18:11:14 +02:00 committed by GitHub
parent 9986a21970
commit ece4a1cd7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1287 additions and 1092 deletions

View file

@ -1,14 +1,14 @@
[
{
"label": "Debug Zed with LLDB",
"adapter": "lldb",
"adapter": "LLDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug Zed with GDB",
"adapter": "gdb",
"adapter": "GDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-right-icon lucide-arrow-down-right"><path d="m7 7 10 10"/><path d="M17 7v10H7"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View file

@ -1,3 +1 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2H6.5C6.5 1.86739 6.44732 1.74021 6.35355 1.64645C6.25979 1.55268 6.13261 1.5 6 1.5V2ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2C1.5 2.27614 1.72386 2.5 2 2.5L2 1.5ZM5.5 6C5.5 6.27614 5.72386 6.5 6 6.5C6.27614 6.5 6.5 6.27614 6.5 6H5.5ZM1.64645 5.64645C1.45118 5.84171 1.45118 6.15829 1.64645 6.35355C1.84171 6.54882 2.15829 6.54882 2.35355 6.35355L1.64645 5.64645ZM6 1.5H2L2 2.5H6V1.5ZM5.5 2V6H6.5V2H5.5ZM5.64645 1.64645L1.64645 5.64645L2.35355 6.35355L6.35355 2.35355L5.64645 1.64645Z" fill="white"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 296 B

Before After
Before After

1
assets/icons/bug_off.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-off-icon lucide-bug-off"><path d="M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"/><path d="M14.12 3.88 16 2"/><path d="M22 13h-4v-2a4 4 0 0 0-4-4h-1.3"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="m2 2 20 20"/><path d="M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"/><path d="M12 20v-8"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/></svg>

After

Width:  |  Height:  |  Size: 551 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off-icon lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>

After

Width:  |  Height:  |  Size: 357 B

1
assets/icons/power.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-power-icon lucide-power"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>

After

Width:  |  Height:  |  Size: 294 B

View file

@ -78,10 +78,10 @@ impl DebugAdapter for LldbDebugAdapter {
match &config.request {
DebugRequestType::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
map.insert("stopOnEntry".into(), config.stop_on_entry.into());
}
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("stopOnEntry".into(), config.stop_on_entry.into());
map.insert("args".into(), launch.args.clone().into());
map.insert(
"cwd".into(),

View file

@ -126,24 +126,31 @@ impl DebugAdapter for PythonDebugAdapter {
}
fn request_args(&self, config: &DebugTaskDefinition) -> Value {
let mut args = json!({
"request": match config.request {
DebugRequestType::Launch(_) => "launch",
DebugRequestType::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequestType::Launch(launch_config) => {
json!({
"program": launch_config.program,
"args": launch_config.args,
"subProcess": true,
"cwd": launch_config.cwd,
"redirectOutput": true,
"StopOnEntry": config.stop_on_entry,
})
DebugRequestType::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
dap::DebugRequestType::Attach(attach_config) => {
json!({
"subProcess": true,
"redirectOutput": true,
"processId": attach_config.process_id
})
DebugRequestType::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
args
}
}

View file

@ -54,6 +54,7 @@ impl AttachModal {
pub fn new(
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -74,13 +75,14 @@ impl AttachModal {
})
.collect();
processes.sort_by_key(|k| k.name.clone());
Self::with_processes(project, debug_config, processes, window, cx)
Self::with_processes(project, debug_config, processes, modal, window, cx)
}
pub(super) fn with_processes(
project: Entity<project::Project>,
debug_config: task::DebugTaskDefinition,
processes: Vec<Candidate>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -103,6 +105,7 @@ impl AttachModal {
window,
cx,
)
.modal(modal)
});
Self {
_subscription: cx.subscribe(&picker, |_, _, _, cx| {

View file

@ -1,4 +1,8 @@
use crate::session::DebugSession;
use crate::{
ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
};
use crate::{new_session_modal::NewSessionModal, session::DebugSession};
use anyhow::{Result, anyhow};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
@ -13,7 +17,10 @@ use gpui::{
};
use project::{
Project,
debugger::dap_store::{self, DapStore},
debugger::{
dap_store::{self, DapStore},
session::ThreadStatus,
},
terminals::TerminalKind,
};
use rpc::proto::{self};
@ -21,11 +28,9 @@ use settings::Settings;
use std::{any::TypeId, path::PathBuf};
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::prelude::*;
use util::ResultExt;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use workspace::{
ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut,
StepOver, Stop, ToggleIgnoreBreakpoints, Workspace,
Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent},
pane,
};
@ -51,10 +56,11 @@ actions!(debug_panel, [ToggleFocus]);
pub struct DebugPanel {
size: Pixels,
pane: Entity<Pane>,
/// This represents the last debug definition that was created in the new session modal
pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
pub(crate) last_inert_config: Option<DebugTaskDefinition>,
}
impl DebugPanel {
@ -66,8 +72,6 @@ impl DebugPanel {
cx.new(|cx| {
let project = workspace.project().clone();
let dap_store = project.read(cx).dap_store();
let weak_workspace = workspace.weak_handle();
let debug_panel = cx.weak_entity();
let pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
@ -81,71 +85,9 @@ impl DebugPanel {
pane.set_can_split(None);
pane.set_can_navigate(true, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_window, _cx| true);
pane.set_should_display_tab_bar(|_window, _cx| false);
pane.set_close_pane_if_empty(true, cx);
pane.set_render_tab_bar_buttons(cx, {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
let debug_panel = debug_panel.clone();
move |_, _, cx| {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
(
None,
Some(
h_flex()
.child(
IconButton::new("new-debug-session", IconName::Plus)
.icon_size(IconSize::Small)
.on_click({
let debug_panel = debug_panel.clone();
cx.listener(move |pane, _, window, cx| {
let config = debug_panel
.read_with(cx, |this: &DebugPanel, _| {
this.last_inert_config.clone()
})
.log_err()
.flatten();
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
debug_panel.clone(),
config,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
})
}),
)
.into_any_element(),
),
)
}
});
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
debug_panel.clone(),
None,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
pane
});
@ -159,7 +101,7 @@ impl DebugPanel {
pane,
size: px(300.),
_subscriptions,
last_inert_config: None,
past_debug_definition: None,
project: project.downgrade(),
workspace: workspace.weak_handle(),
};
@ -295,7 +237,7 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
return log::error!(
"Couldn't get session with id: {session_id:?} from DebugClientStarted event"
@ -470,6 +412,274 @@ impl DebugPanel {
_ => {}
}
}
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let active_session = self
.pane
.read(cx)
.active_item()
.and_then(|item| item.downcast::<DebugSession>());
Some(
h_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.p_1()
.justify_between()
.w_full()
.child(
h_flex().gap_2().w_full().when_some(
active_session
.as_ref()
.and_then(|session| session.read(cx).mode().as_running()),
|this, running_session| {
let thread_status = running_session
.read(cx)
.thread_status(cx)
.unwrap_or(project::debugger::session::ThreadStatus::Exited);
let capabilities = running_session.read(cx).capabilities(cx);
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
IconButton::new("debug-pause", IconName::DebugPause)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.pause_thread(cx);
},
))
.tooltip(move |window, cx| {
Tooltip::text("Pause program")(window, cx)
}),
)
} else {
this.child(
IconButton::new("debug-continue", IconName::DebugContinue)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| this.continue_thread(cx),
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Continue program")(window, cx)
}),
)
}
})
.child(
IconButton::new("debug-step-over", IconName::ArrowRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.step_over(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step over")(window, cx)
}),
)
.child(
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.step_out(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step out")(window, cx)
}),
)
.child(
IconButton::new("debug-step-into", IconName::ArrowDownRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.step_in(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step in")(window, cx)
}),
)
.child(Divider::vertical())
.child(
IconButton::new(
"debug-enable-breakpoint",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
))
.tooltip(move |window, cx| {
Tooltip::text("Disable all breakpoints")(window, cx)
}),
)
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.restart_session(cx);
},
))
.disabled(
!capabilities.supports_restart_request.unwrap_or_default(),
)
.tooltip(move |window, cx| {
Tooltip::text("Restart")(window, cx)
}),
)
.child(
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
|this, _, _window, cx| {
this.stop_thread(cx);
},
))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip({
let label = if capabilities
.supports_terminate_threads_request
.unwrap_or_default()
{
"Terminate Thread"
} else {
"Terminate all Threads"
};
move |window, cx| Tooltip::text(label)(window, cx)
}),
)
},
),
)
.child(
h_flex()
.gap_2()
.when_some(
active_session
.as_ref()
.and_then(|session| session.read(cx).mode().as_running())
.cloned(),
|this, session| {
this.child(
session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
)
.child(Divider::vertical())
},
)
.when_some(active_session.as_ref(), |this, session| {
let pane = self.pane.downgrade();
let label = session.read(cx).label(cx);
this.child(DropdownMenu::new(
"debugger-session-list",
label,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let sessions = pane
.read_with(cx, |pane, _| {
pane.items().map(|item| item.boxed_clone()).collect()
})
.ok()
.unwrap_or_else(Vec::new);
for (index, item) in sessions.into_iter().enumerate() {
if let Some(session) = item.downcast::<DebugSession>() {
let pane = pane.clone();
this = this.entry(
session.read(cx).label(cx),
None,
move |window, cx| {
pane.update(cx, |pane, cx| {
pane.activate_item(
index, true, true, window, cx,
);
})
.ok();
},
);
}
}
this
}),
))
.child(Divider::vertical())
})
.child(
IconButton::new("debug-new-session", IconName::Plus)
.icon_size(IconSize::Small)
.on_click({
let workspace = self.workspace.clone();
let weak_panel = cx.weak_entity();
let past_debug_definition = self.past_debug_definition.clone();
move |_, window, cx| {
let weak_panel = weak_panel.clone();
let past_debug_definition = past_debug_definition.clone();
let _ = workspace.update(cx, |this, cx| {
let workspace = cx.weak_entity();
this.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
past_debug_definition,
weak_panel,
workspace,
window,
cx,
)
});
});
}
})
.tooltip(|window, cx| {
Tooltip::for_action(
"New Debug Session",
&CreateDebuggingSession,
window,
cx,
)
}),
),
),
)
}
}
impl EventEmitter<PanelEvent> for DebugPanel {}
@ -507,7 +717,7 @@ impl Panel for DebugPanel {
) {
}
fn size(&self, _window: &Window, _cx: &App) -> Pixels {
fn size(&self, _window: &Window, _: &App) -> Pixels {
self.size
}
@ -538,42 +748,49 @@ impl Panel for DebugPanel {
fn activation_priority(&self) -> u32 {
9
}
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
if active && self.pane.read(cx).items_len() == 0 {
let Some(project) = self.project.clone().upgrade() else {
return;
};
let config = self.last_inert_config.clone();
let panel = cx.weak_entity();
// todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
self.pane.update(cx, |this, cx| {
this.add_item(
Box::new(DebugSession::inert(
project,
self.workspace.clone(),
panel,
config,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
});
}
}
fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
}
impl Render for DebugPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_sessions = self.pane.read(cx).items_len() > 0;
v_flex()
.key_context("DebugPanel")
.track_focus(&self.focus_handle(cx))
.size_full()
.child(self.pane.clone())
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
.track_focus(&self.focus_handle(cx))
.map(|this| {
if has_sessions {
this.child(self.pane.clone())
} else {
this.child(
v_flex()
.h_full()
.gap_1()
.items_center()
.justify_center()
.child(
h_flex().child(
Label::new("No Debugging Sessions")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
h_flex().flex_shrink().child(
Button::new("spawn-new-session-empty-state", "New Session")
.size(ButtonSize::Large)
.on_click(|_, window, cx| {
window.dispatch_action(
CreateDebuggingSession.boxed_clone(),
cx,
);
}),
),
),
)
}
})
.into_any()
}
}

View file

@ -1,20 +1,38 @@
use dap::debugger_settings::DebuggerSettings;
use debugger_panel::{DebugPanel, ToggleFocus};
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::App;
use gpui::{App, actions};
use new_session_modal::NewSessionModal;
use session::DebugSession;
use settings::Settings;
use workspace::{
Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
ToggleIgnoreBreakpoints, Workspace,
};
use workspace::{ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
pub mod session;
mod new_session_modal;
pub(crate) mod session;
#[cfg(test)]
mod tests;
pub mod tests;
actions!(
debugger,
[
Start,
Continue,
Disconnect,
Pause,
Restart,
StepInto,
StepOver,
StepOut,
StepBack,
Stop,
ToggleIgnoreBreakpoints,
ClearAllBreakpoints,
CreateDebuggingSession,
]
);
pub fn init(cx: &mut App) {
DebuggerSettings::register(cx);
@ -115,6 +133,23 @@ pub fn init(cx: &mut App) {
})
})
},
)
.register_action(
|workspace: &mut Workspace, _: &CreateDebuggingSession, window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
window,
cx,
)
});
},
);
})
})

View file

@ -0,0 +1,633 @@
use std::{
borrow::Cow,
ops::Not,
path::{Path, PathBuf},
};
use anyhow::{Result, anyhow};
use dap::DebugRequestType;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
WeakEntity,
};
use settings::Settings;
use task::{DebugTaskDefinition, LaunchConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
#[derive(Clone)]
pub(super) struct NewSessionModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
mode: NewSessionMode,
stop_on_entry: ToggleState,
debugger: Option<SharedString>,
last_selected_profile_name: Option<SharedString>,
}
fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
match request {
DebugRequestType::Launch(config) => {
let last_path_component = Path::new(&config.program)
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed(&config.program));
format!("{} ({debugger})", last_path_component)
}
DebugRequestType::Attach(config) => format!(
"pid: {} ({debugger})",
config.process_id.unwrap_or(u32::MAX)
),
}
}
impl NewSessionModal {
pub(super) fn new(
past_debug_definition: Option<DebugTaskDefinition>,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Self {
let debugger = past_debug_definition
.as_ref()
.map(|def| def.adapter.clone().into());
let stop_on_entry = past_debug_definition
.as_ref()
.and_then(|def| def.stop_on_entry);
let launch_config = match past_debug_definition.map(|def| def.request) {
Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
_ => None,
};
Self {
workspace: workspace.clone(),
debugger,
debug_panel,
mode: NewSessionMode::launch(launch_config, window, cx),
stop_on_entry: stop_on_entry
.map(Into::into)
.unwrap_or(ToggleState::Unselected),
last_selected_profile_name: None,
}
}
fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
let request = self.mode.debug_task(cx);
Some(DebugTaskDefinition {
adapter: self.debugger.clone()?.to_string(),
label: suggested_label(&request, self.debugger.as_deref()?),
request,
initialize_args: None,
tcp_connection: None,
locator: None,
stop_on_entry: match self.stop_on_entry {
ToggleState::Selected => Some(true),
_ => None,
},
})
}
fn start_new_session(&self, cx: &mut Context<Self>) -> Result<()> {
let workspace = self.workspace.clone();
let config = self
.debug_config(cx)
.ok_or_else(|| anyhow!("Failed to create a debug config"))?;
let _ = self.debug_panel.update(cx, |panel, _| {
panel.past_debug_definition = Some(config.clone());
});
cx.spawn(async move |this, cx| {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
let task =
project.update(cx, |this, cx| this.start_debug_session(config.into(), cx))?;
let spawn_result = task.await;
if spawn_result.is_ok() {
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
spawn_result?;
anyhow::Result::<_, anyhow::Error>::Ok(())
})
.detach_and_log_err(cx);
Ok(())
}
fn update_attach_picker(
attach: &Entity<AttachMode>,
selected_debugger: &str,
window: &mut Window,
cx: &mut App,
) {
attach.update(cx, |this, cx| {
if selected_debugger != this.debug_definition.adapter {
this.debug_definition.adapter = selected_debugger.into();
if let Some(project) = this
.workspace
.read_with(cx, |workspace, _| workspace.project().clone())
.ok()
{
this.attach_picker = Some(cx.new(|cx| {
let modal = AttachModal::new(
project,
this.debug_definition.clone(),
false,
window,
cx,
);
window.focus(&modal.focus_handle(cx));
modal
}));
}
}
cx.notify();
})
}
fn adapter_drop_down_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> ui::DropdownMenu {
let workspace = self.workspace.clone();
let weak = cx.weak_entity();
let debugger = self.debugger.clone();
DropdownMenu::new(
"dap-adapter-picker",
debugger
.as_ref()
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
.clone(),
ContextMenu::build(window, cx, move |mut menu, _, cx| {
let setter_for_name = |name: SharedString| {
let weak = weak.clone();
move |window: &mut Window, cx: &mut App| {
weak.update(cx, |this, cx| {
this.debugger = Some(name.clone());
cx.notify();
if let NewSessionMode::Attach(attach) = &this.mode {
Self::update_attach_picker(&attach, &name, window, cx);
}
})
.ok();
}
};
let available_adapters = workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.debug_adapters()
.enumerate_adapters()
})
.ok()
.unwrap_or_default();
for adapter in available_adapters {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
}
menu
}),
)
}
fn debug_config_drop_down_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> ui::DropdownMenu {
let workspace = self.workspace.clone();
let weak = cx.weak_entity();
let last_profile = self.last_selected_profile_name.clone();
DropdownMenu::new(
"debug-config-menu",
last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
ContextMenu::build(window, cx, move |mut menu, _, cx| {
let setter_for_name = |task: DebugTaskDefinition| {
let weak = weak.clone();
let workspace = workspace.clone();
move |window: &mut Window, cx: &mut App| {
weak.update(cx, |this, cx| {
this.last_selected_profile_name = Some(SharedString::from(&task.label));
this.debugger = Some(task.adapter.clone().into());
match &task.request {
DebugRequestType::Launch(launch_config) => {
this.mode = NewSessionMode::launch(
Some(launch_config.clone()),
window,
cx,
);
}
DebugRequestType::Attach(_) => {
this.mode = NewSessionMode::attach(
this.debugger.clone(),
workspace.clone(),
window,
cx,
);
if let Some((debugger, attach)) =
this.debugger.as_ref().zip(this.mode.as_attach())
{
Self::update_attach_picker(&attach, &debugger, window, cx);
}
}
}
cx.notify();
})
.ok();
}
};
let available_adapters: Vec<DebugTaskDefinition> = workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.iter()
.flat_map(|task_inventory| task_inventory.read(cx).list_debug_tasks())
.cloned()
.filter_map(|task| task.try_into().ok())
.collect()
})
.ok()
.unwrap_or_default();
for debug_definition in available_adapters {
menu = menu.entry(
debug_definition.label.clone(),
None,
setter_for_name(debug_definition),
);
}
menu
}),
)
}
}
#[derive(Clone)]
struct LaunchMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
}
impl LaunchMode {
fn new(
past_launch_config: Option<LaunchConfig>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let (past_program, past_cwd) = past_launch_config
.map(|config| (Some(config.program), config.cwd))
.unwrap_or_else(|| (None, None));
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
};
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
this.set_placeholder_text("Working Directory", cx);
if let Some(past_cwd) = past_cwd {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
cx.new(|_| Self { program, cwd })
}
fn debug_task(&self, cx: &App) -> task::LaunchConfig {
let path = self.cwd.read(cx).text(cx);
task::LaunchConfig {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
}
}
}
#[derive(Clone)]
struct AttachMode {
workspace: WeakEntity<Workspace>,
debug_definition: DebugTaskDefinition,
attach_picker: Option<Entity<AttachModal>>,
focus_handle: FocusHandle,
}
impl AttachMode {
fn new(
debugger: Option<SharedString>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let debug_definition = DebugTaskDefinition {
label: "Attach New Session Setup".into(),
request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
tcp_connection: None,
adapter: debugger.clone().unwrap_or_default().into(),
locator: None,
initialize_args: None,
stop_on_entry: Some(false),
};
let attach_picker = if let Some(project) = debugger.and(
workspace
.read_with(cx, |workspace, _| workspace.project().clone())
.ok(),
) {
Some(cx.new(|cx| {
let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
}))
} else {
None
};
cx.new(|cx| Self {
workspace,
debug_definition,
attach_picker,
focus_handle: cx.focus_handle(),
})
}
fn debug_task(&self) -> task::AttachConfig {
task::AttachConfig { process_id: None }
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
#[derive(Clone)]
enum NewSessionMode {
Launch(Entity<LaunchMode>),
Attach(Entity<AttachMode>),
}
impl NewSessionMode {
fn debug_task(&self, cx: &App) -> DebugRequestType {
match self {
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
}
}
fn as_attach(&self) -> Option<&Entity<AttachMode>> {
if let NewSessionMode::Attach(entity) = self {
Some(entity)
} else {
None
}
}
}
impl Focusable for NewSessionMode {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self {
NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
NewSessionMode::Attach(entity) => entity.read(cx).focus_handle.clone(),
}
}
}
impl RenderOnce for LaunchMode {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.p_2()
.w_full()
.gap_3()
.track_focus(&self.program.focus_handle(cx))
.child(
div().child(
Label::new("Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.program, window, cx))
.child(
div().child(
Label::new("Working Directory")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.cwd, window, cx))
}
}
impl RenderOnce for AttachMode {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
v_flex().w_full().children(self.attach_picker.clone())
}
}
impl RenderOnce for NewSessionMode {
fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
match self {
NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
}
}
}
impl NewSessionMode {
fn attach(
debugger: Option<SharedString>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Self {
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
}
fn launch(past_launch_config: Option<LaunchConfig>, window: &mut Window, cx: &mut App) -> Self {
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
}
}
fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
background_color: Some(theme.colors().editor_background),
..Default::default()
};
let element = EditorElement::new(
editor,
EditorStyle {
background: theme.colors().editor_background,
local_player: theme.players().local(),
text: text_style,
..Default::default()
},
);
div()
.rounded_md()
.p_1()
.border_1()
.border_color(theme.colors().border_variant)
.when(
editor.focus_handle(cx).contains_focused(window, cx),
|this| this.border_color(theme.colors().border_focused),
)
.child(element)
.bg(theme.colors().editor_background)
}
impl Render for NewSessionModal {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
v_flex()
.size_full()
.w(rems(34.))
.elevation_3(cx)
.bg(cx.theme().colors().elevated_surface_background)
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
cx.emit(DismissEvent);
}))
.child(
h_flex()
.w_full()
.justify_around()
.p_2()
.child(
h_flex()
.justify_start()
.w_full()
.child(
ToggleButton::new(
"debugger-session-ui-launch-button",
"New Session",
)
.size(ButtonSize::Default)
.style(ui::ButtonStyle::Subtle)
.toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::launch(None, window, cx);
this.mode.focus_handle(cx).focus(window);
cx.notify();
}))
.first(),
)
.child(
ToggleButton::new(
"debugger-session-ui-attach-button",
"Attach to Process",
)
.size(ButtonSize::Default)
.toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
.style(ui::ButtonStyle::Subtle)
.on_click(cx.listener(|this, _, window, cx| {
this.mode = NewSessionMode::attach(
this.debugger.clone(),
this.workspace.clone(),
window,
cx,
);
if let Some((debugger, attach)) =
this.debugger.as_ref().zip(this.mode.as_attach())
{
Self::update_attach_picker(&attach, &debugger, window, cx);
}
this.mode.focus_handle(cx).focus(window);
cx.notify();
}))
.last(),
),
)
.justify_between()
.child(self.adapter_drop_down_menu(window, cx))
.border_color(cx.theme().colors().border_variant)
.border_b_1(),
)
.child(v_flex().child(self.mode.clone().render(window, cx)))
.child(
h_flex()
.justify_between()
.gap_2()
.p_2()
.border_color(cx.theme().colors().border_variant)
.border_t_1()
.w_full()
.child(self.debug_config_drop_down_menu(window, cx))
.child(
h_flex()
.justify_end()
.when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
let weak = cx.weak_entity();
this.child(
CheckboxWithLabel::new(
"debugger-stop-on-entry",
Label::new("Stop on Entry").size(ui::LabelSize::Small),
self.stop_on_entry,
move |state, _, cx| {
weak.update(cx, |this, _| {
this.stop_on_entry = *state;
})
.ok();
},
)
.checkbox_position(ui::IconPosition::End),
)
})
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, _, cx| {
this.start_new_session(cx).log_err();
}))
.disabled(self.debugger.is_none()),
),
),
)
}
}
impl EventEmitter<DismissEvent> for NewSessionModal {}
impl Focusable for NewSessionModal {
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
self.mode.focus_handle(cx)
}
}
impl ModalView for NewSessionModal {}

View file

@ -1,26 +1,13 @@
mod failed;
mod inert;
pub mod running;
mod starting;
use std::time::Duration;
use dap::client::SessionId;
use failed::FailedState;
use gpui::{
Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, Task, Transformation, WeakEntity, percentage,
};
use inert::{InertEvent, InertState};
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use project::Project;
use project::debugger::{dap_store::DapStore, session::Session};
use project::worktree_store::WorktreeStore;
use rpc::proto::{self, PeerId};
use running::RunningState;
use starting::{StartingEvent, StartingState};
use task::DebugTaskDefinition;
use ui::{Indicator, prelude::*};
use util::ResultExt;
use ui::prelude::*;
use workspace::{
FollowableItem, ViewId, Workspace,
item::{self, Item},
@ -29,9 +16,6 @@ use workspace::{
use crate::debugger_panel::DebugPanel;
pub(crate) enum DebugSessionState {
Inert(Entity<InertState>),
Starting(Entity<StartingState>),
Failed(Entity<FailedState>),
Running(Entity<running::RunningState>),
}
@ -39,7 +23,6 @@ impl DebugSessionState {
pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> {
match &self {
DebugSessionState::Running(entity) => Some(entity),
_ => None,
}
}
}
@ -48,9 +31,9 @@ pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
mode: DebugSessionState,
dap_store: WeakEntity<DapStore>,
debug_panel: WeakEntity<DebugPanel>,
worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_debug_panel: WeakEntity<DebugPanel>,
_worktree_store: WeakEntity<WorktreeStore>,
_workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
}
@ -69,46 +52,11 @@ pub enum ThreadItem {
}
impl DebugSession {
pub(super) fn inert(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
config: Option<DebugTaskDefinition>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let default_cwd = project
.read(cx)
.worktrees(cx)
.next()
.and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
.unwrap_or_default();
let inert =
cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, config, window, cx));
let project = project.read(cx);
let dap_store = project.dap_store().downgrade();
let worktree_store = project.worktree_store().downgrade();
cx.new(|cx| {
let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
Self {
remote_id: None,
mode: DebugSessionState::Inert(inert),
dap_store,
worktree_store,
debug_panel,
workspace,
_subscriptions,
}
})
}
pub(crate) fn running(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
session: Entity<Session>,
debug_panel: WeakEntity<DebugPanel>,
_debug_panel: WeakEntity<DebugPanel>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
@ -121,26 +69,20 @@ impl DebugSession {
remote_id: None,
mode: DebugSessionState::Running(mode),
dap_store: project.read(cx).dap_store().downgrade(),
debug_panel,
worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
_debug_panel,
_worktree_store: project.read(cx).worktree_store().downgrade(),
_workspace: workspace,
})
}
pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> {
match &self.mode {
DebugSessionState::Inert(_) => None,
DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id),
DebugSessionState::Failed(_) => None,
DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
}
}
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
match &self.mode {
DebugSessionState::Inert(_) => {}
DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit)
DebugSessionState::Failed(_) => {}
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
}
}
@ -149,63 +91,29 @@ impl DebugSession {
&self.mode
}
fn on_inert_event(
&mut self,
_: &Entity<InertState>,
event: &InertEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let dap_store = self.dap_store.clone();
let InertEvent::Spawned { config } = event;
let config = config.clone();
self.debug_panel
.update(cx, |this, _| this.last_inert_config = Some(config.clone()))
.log_err();
let worktree = self
.worktree_store
.update(cx, |this, _| this.worktrees().next())
.ok()
.flatten()
.expect("worktree-less project");
let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
store.new_session(config.into(), &worktree, None, cx)
}) else {
return;
pub(crate) fn label(&self, cx: &App) -> String {
let session_id = match &self.mode {
DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
};
let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx));
self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
self.mode = DebugSessionState::Starting(starting);
}
fn on_starting_event(
&mut self,
_: &Entity<StartingState>,
event: &StartingEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let StartingEvent::Finished(session) = event {
let mode =
cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
self.mode = DebugSessionState::Running(mode);
} else if let StartingEvent::Failed = event {
self.mode = DebugSessionState::Failed(cx.new(FailedState::new));
let Ok(Some(session)) = self
.dap_store
.read_with(cx, |store, _| store.session_by_id(session_id))
else {
return "".to_owned();
};
cx.notify();
session
.read(cx)
.as_local()
.expect("Remote Debug Sessions are not implemented yet")
.label()
}
}
impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
impl Focusable for DebugSession {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx),
DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
}
}
@ -213,61 +121,6 @@ impl Focusable for DebugSession {
impl Item for DebugSession {
type Event = DebugPanelItemEvent;
fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
let (icon, label, color) = match &self.mode {
DebugSessionState::Inert(_) => (None, "New Session", Color::Default),
DebugSessionState::Starting(_) => (None, "Starting", Color::Default),
DebugSessionState::Failed(_) => (
Some(Indicator::dot().color(Color::Error)),
"Failed",
Color::Error,
),
DebugSessionState::Running(state) => {
if state.read(cx).session().read(cx).is_terminated() {
(
Some(Indicator::dot().color(Color::Error)),
"Terminated",
Color::Error,
)
} else {
match state.read(cx).thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => (
Some(Indicator::dot().color(Color::Conflict)),
state
.read_with(cx, |state, cx| state.thread_status(cx))
.map(|status| status.label())
.unwrap_or("Stopped"),
Color::Conflict,
),
_ => (
Some(Indicator::dot().color(Color::Success)),
state
.read_with(cx, |state, cx| state.thread_status(cx))
.map(|status| status.label())
.unwrap_or("Running"),
Color::Success,
),
}
}
}
};
let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
h_flex()
.gap_2()
.children(is_starting.then(|| {
Icon::new(IconName::ArrowCircle).with_animation(
"starting-debug-session",
Animation::new(Duration::from_secs(2)).repeat(),
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
)
}))
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(Label::new(label).color(color))
.into_any_element()
}
}
impl FollowableItem for DebugSession {
@ -339,15 +192,6 @@ impl FollowableItem for DebugSession {
impl Render for DebugSession {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
match &self.mode {
DebugSessionState::Inert(inert_state) => {
inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Starting(starting_state) => {
starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Failed(failed_state) => {
failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Running(running_state) => {
running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}

View file

@ -1,30 +0,0 @@
use gpui::{FocusHandle, Focusable};
use ui::{
Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window, h_flex,
};
pub(crate) struct FailedState {
focus_handle: FocusHandle,
}
impl FailedState {
pub(super) fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Focusable for FailedState {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for FailedState {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
h_flex()
.size_full()
.items_center()
.justify_center()
.child(Label::new("Failed to spawn debugging session").color(Color::Error))
}
}

View file

@ -1,337 +0,0 @@
use std::path::PathBuf;
use dap::DebugRequestType;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
use settings::Settings as _;
use task::{DebugTaskDefinition, LaunchConfig, TCPHost};
use theme::ThemeSettings;
use ui::{
ActiveTheme as _, ButtonCommon, ButtonLike, Clickable, Context, ContextMenu, Disableable,
DropdownMenu, FluentBuilder, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
SplitButton, Styled, Window, div, h_flex, relative, v_flex,
};
use workspace::Workspace;
use crate::attach_modal::AttachModal;
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
enum SpawnMode {
#[default]
Launch,
Attach,
}
impl SpawnMode {
fn label(&self) -> &'static str {
match self {
SpawnMode::Launch => "Launch",
SpawnMode::Attach => "Attach",
}
}
}
impl From<DebugRequestType> for SpawnMode {
fn from(request: DebugRequestType) -> Self {
match request {
DebugRequestType::Launch(_) => SpawnMode::Launch,
DebugRequestType::Attach(_) => SpawnMode::Attach,
}
}
}
pub(crate) struct InertState {
focus_handle: FocusHandle,
selected_debugger: Option<SharedString>,
program_editor: Entity<Editor>,
cwd_editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
spawn_mode: SpawnMode,
popover_handle: PopoverMenuHandle<ContextMenu>,
}
impl InertState {
pub(super) fn new(
workspace: WeakEntity<Workspace>,
default_cwd: &str,
debug_config: Option<DebugTaskDefinition>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let selected_debugger = debug_config
.as_ref()
.map(|config| SharedString::from(config.adapter.clone()));
let spawn_mode = debug_config
.as_ref()
.map(|config| config.request.clone().into())
.unwrap_or_default();
let program = debug_config
.as_ref()
.and_then(|config| match &config.request {
DebugRequestType::Attach(_) => None,
DebugRequestType::Launch(launch_config) => Some(launch_config.program.clone()),
});
let program_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
if let Some(program) = program {
editor.insert(&program, window, cx);
} else {
editor.set_placeholder_text("Program path", cx);
}
editor
});
let cwd = debug_config
.and_then(|config| match &config.request {
DebugRequestType::Attach(_) => None,
DebugRequestType::Launch(launch_config) => launch_config.cwd.clone(),
})
.unwrap_or_else(|| PathBuf::from(default_cwd));
let cwd_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.insert(cwd.to_str().unwrap_or_else(|| default_cwd), window, cx);
editor.set_placeholder_text("Working directory", cx);
editor
});
Self {
workspace,
cwd_editor,
program_editor,
selected_debugger,
spawn_mode,
focus_handle: cx.focus_handle(),
popover_handle: Default::default(),
}
}
}
impl Focusable for InertState {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
pub(crate) enum InertEvent {
Spawned { config: DebugTaskDefinition },
}
impl EventEmitter<InertEvent> for InertState {}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
impl Render for InertState {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<'_, Self>,
) -> impl ui::IntoElement {
let weak = cx.weak_entity();
let workspace = self.workspace.clone();
let disable_buttons = self.selected_debugger.is_none();
let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
.child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
.on_click(cx.listener(|this, _, window, cx| {
if this.spawn_mode == SpawnMode::Launch {
let program = this.program_editor.read(cx).text(cx);
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
let kind = this
.selected_debugger
.as_deref()
.unwrap_or_else(|| {
unimplemented!(
"Automatic selection of a debugger based on users project"
)
})
.to_string();
cx.emit(InertEvent::Spawned {
config: DebugTaskDefinition {
label: "hard coded".into(),
adapter: kind,
request: DebugRequestType::Launch(LaunchConfig {
program,
cwd: Some(cwd),
args: Default::default(),
}),
tcp_connection: Some(TCPHost::default()),
initialize_args: None,
locator: None,
stop_on_entry: None,
},
});
} else {
this.attach(window, cx)
}
}))
.disabled(disable_buttons);
v_flex()
.track_focus(&self.focus_handle)
.size_full()
.gap_1()
.p_2()
.child(
v_flex()
.gap_1()
.child(
h_flex()
.w_full()
.gap_2()
.child(Self::render_editor(&self.program_editor, cx))
.child(
h_flex().child(DropdownMenu::new(
"dap-adapter-picker",
self.selected_debugger
.as_ref()
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
.clone(),
ContextMenu::build(window, cx, move |mut this, _, cx| {
let setter_for_name = |name: SharedString| {
let weak = weak.clone();
move |_: &mut Window, cx: &mut App| {
let name = name.clone();
weak.update(cx, move |this, cx| {
this.selected_debugger = Some(name.clone());
cx.notify();
})
.ok();
}
};
let available_adapters = workspace
.update(cx, |this, cx| {
this.project()
.read(cx)
.debug_adapters()
.enumerate_adapters()
})
.ok()
.unwrap_or_default();
for adapter in available_adapters {
this = this.entry(
adapter.0.clone(),
None,
setter_for_name(adapter.0.clone()),
);
}
this
}),
)),
),
)
.child(
h_flex()
.gap_2()
.child(Self::render_editor(&self.cwd_editor, cx))
.map(|this| {
let entity = cx.weak_entity();
this.child(SplitButton {
left: spawn_button,
right: PopoverMenu::new("debugger-select-spawn-mode")
.trigger(
ButtonLike::new_rounded_right(
"debugger-spawn-button-mode",
)
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::None)
.child(
div().px_1().child(
Icon::new(IconName::ChevronDownSmall)
.size(IconSize::XSmall),
),
),
)
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, {
let entity = entity.clone();
move |this, _, _| {
this.entry("Launch", None, {
let entity = entity.clone();
move |_, cx| {
let _ =
entity.update(cx, |this, cx| {
this.spawn_mode =
SpawnMode::Launch;
cx.notify();
});
}
})
.entry("Attach", None, {
let entity = entity.clone();
move |_, cx| {
let _ =
entity.update(cx, |this, cx| {
this.spawn_mode =
SpawnMode::Attach;
cx.notify();
});
}
})
}
}))
})
.with_handle(self.popover_handle.clone())
.into_any_element(),
})
}),
),
)
}
}
impl InertState {
fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
EditorElement::new(
editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
let kind = self
.selected_debugger
.as_deref()
.map(|s| s.to_string())
.unwrap_or_else(|| {
unimplemented!("Automatic selection of a debugger based on users project")
});
let config = DebugTaskDefinition {
label: "hard coded attach".into(),
adapter: kind,
request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
initialize_args: None,
locator: None,
tcp_connection: Some(TCPHost::default()),
stop_on_entry: None,
};
let _ = self.workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::new(project, config, window, cx)
});
});
}
}

View file

@ -15,10 +15,9 @@ use rpc::proto::ViewId;
use settings::Settings;
use stack_frame_list::StackFrameList;
use ui::{
ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context, ContextMenu,
Disableable, Divider, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, Indicator,
InteractiveElement, IntoElement, Label, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Tooltip, Window, div, h_flex, v_flex,
ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
};
use util::ResultExt;
use variable_list::VariableList;
@ -42,7 +41,7 @@ pub struct RunningState {
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let threads = self.session.update(cx, |this, cx| this.threads(cx));
self.select_current_thread(&threads, cx);
@ -51,255 +50,27 @@ impl Render for RunningState {
.map(|thread_id| self.session.read(cx).thread_status(thread_id))
.unwrap_or(ThreadStatus::Exited);
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
self.variable_list.update(cx, |this, cx| {
this.disabled(thread_status != ThreadStatus::Stopped, cx);
});
let active_thread_item = &self.active_thread_item;
let has_no_threads = threads.is_empty();
let capabilities = self.capabilities(cx);
let state = cx.entity();
h_flex()
.key_context("DebugPanelItem")
.track_focus(&self.focus_handle(cx))
.size_full()
.items_start()
.child(
v_flex()
.size_full()
.items_start()
.child(
h_flex()
.w_full()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(
h_flex()
.px_1()
.py_0p5()
.w_full()
.gap_1()
.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
IconButton::new(
"debug-pause",
IconName::DebugPause,
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.pause_thread(cx);
}))
.tooltip(move |window, cx| {
Tooltip::text("Pause program")(window, cx)
}),
)
} else {
this.child(
IconButton::new(
"debug-continue",
IconName::DebugContinue,
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.continue_thread(cx)
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Continue program")(window, cx)
}),
)
}
})
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.restart_session(cx);
}))
.disabled(
!capabilities
.supports_restart_request
.unwrap_or_default(),
)
.tooltip(move |window, cx| {
Tooltip::text("Restart")(window, cx)
}),
)
.child(
IconButton::new("debug-stop", IconName::DebugStop)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.stop_thread(cx);
}))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip({
let label = if capabilities
.supports_terminate_threads_request
.unwrap_or_default()
{
"Terminate Thread"
} else {
"Terminate all Threads"
};
move |window, cx| Tooltip::text(label)(window, cx)
}),
)
.child(
IconButton::new(
"debug-disconnect",
IconName::DebugDisconnect,
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.disconnect_client(cx);
}))
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.tooltip(Tooltip::text("Disconnect")),
)
.child(Divider::vertical())
.when(
capabilities.supports_step_back.unwrap_or(false),
|this| {
this.child(
IconButton::new(
"debug-step-back",
IconName::DebugStepBack,
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_back(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step back")(window, cx)
}),
)
},
)
.child(
IconButton::new("debug-step-over", IconName::DebugStepOver)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_over(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step over")(window, cx)
}),
)
.child(
IconButton::new("debug-step-in", IconName::DebugStepInto)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_in(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step in")(window, cx)
}),
)
.child(
IconButton::new("debug-step-out", IconName::DebugStepOut)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_out(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step out")(window, cx)
}),
)
.child(Divider::vertical())
.child(
IconButton::new(
"debug-ignore-breakpoints",
if self.session.read(cx).breakpoints_enabled() {
IconName::DebugBreakpoint
} else {
IconName::DebugIgnoreBreakpoints
},
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
}))
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.tooltip(
move |window, cx| {
Tooltip::text("Ignore breakpoints")(window, cx)
},
),
),
)
.child(
h_flex()
.px_1()
.py_0p5()
.gap_2()
.w_3_4()
.justify_end()
.child(Label::new("Thread:"))
.child(
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build(
window,
cx,
move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(
thread.name,
None,
move |_, cx| {
state.update(cx, |state, cx| {
state.select_thread(
ThreadId(thread_id),
cx,
);
});
},
);
}
this
},
),
)
.disabled(
has_no_threads
|| thread_status != ThreadStatus::Stopped,
),
),
),
)
.child(
h_flex()
.size_full()
.items_start()
.p_1()
.gap_4()
.child(self.stack_frame_list.clone()),
),
v_flex().size_full().items_start().child(
h_flex()
.size_full()
.items_start()
.p_1()
.gap_4()
.child(self.stack_frame_list.clone()),
),
)
.child(
v_flex()
@ -450,37 +221,32 @@ impl RunningState {
self.session_id
}
#[cfg(any(test, feature = "test-support"))]
#[cfg(test)]
pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
self.active_thread_item = thread_item;
cx.notify()
}
#[cfg(any(test, feature = "test-support"))]
#[cfg(test)]
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
&self.stack_frame_list
}
#[cfg(any(test, feature = "test-support"))]
#[cfg(test)]
pub fn console(&self) -> &Entity<Console> {
&self.console
}
#[cfg(any(test, feature = "test-support"))]
pub fn module_list(&self) -> &Entity<ModuleList> {
#[cfg(test)]
pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
&self.module_list
}
#[cfg(any(test, feature = "test-support"))]
pub fn variable_list(&self) -> &Entity<VariableList> {
#[cfg(test)]
pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
&self.variable_list
}
#[cfg(any(test, feature = "test-support"))]
pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
self.session.read(cx).ignore_breakpoints()
}
pub fn capabilities(&self, cx: &App) -> Capabilities {
self.session().read(cx).capabilities().clone()
}
@ -504,8 +270,8 @@ impl RunningState {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn selected_thread_id(&self) -> Option<ThreadId> {
#[cfg(test)]
pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
@ -583,7 +349,7 @@ impl RunningState {
});
}
pub fn step_in(&mut self, cx: &mut Context<Self>) {
pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
@ -595,7 +361,7 @@ impl RunningState {
});
}
pub fn step_out(&mut self, cx: &mut Context<Self>) {
pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
@ -607,7 +373,7 @@ impl RunningState {
});
}
pub fn step_back(&mut self, cx: &mut Context<Self>) {
pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
@ -675,6 +441,10 @@ impl RunningState {
});
}
#[expect(
unused,
reason = "Support for disconnecting a client is not wired through yet"
)]
pub fn disconnect_client(&self, cx: &mut Context<Self>) {
self.session().update(cx, |state, cx| {
state.disconnect_client(cx);
@ -686,6 +456,36 @@ impl RunningState {
session.toggle_ignore_breakpoints(cx).detach();
});
}
pub(crate) fn thread_dropdown(
&self,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> DropdownMenu {
let state = cx.entity();
let threads = self.session.update(cx, |this, cx| this.threads(cx));
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |_, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), cx);
});
});
}
this
}),
)
}
}
impl EventEmitter<DebugPanelItemEvent> for RunningState {}

View file

@ -85,16 +85,11 @@ impl Console {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn editor(&self) -> &Entity<Editor> {
#[cfg(test)]
pub(crate) fn editor(&self) -> &Entity<Editor> {
&self.console
}
#[cfg(any(test, feature = "test-support"))]
pub fn query_bar(&self) -> &Entity<Editor> {
&self.query_bar
}
fn is_local(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
}

View file

@ -147,11 +147,9 @@ impl ModuleList {
)
.into_any()
}
}
#[cfg(any(test, feature = "test-support"))]
impl ModuleList {
pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
#[cfg(test)]
pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
self.session
.update(cx, |session, cx| session.modules(cx).to_vec())
}

View file

@ -87,13 +87,13 @@ impl StackFrameList {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn entries(&self) -> &Vec<StackFrameEntry> {
#[cfg(test)]
pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
&self.entries
}
#[cfg(any(test, feature = "test-support"))]
pub fn flatten_entries(&self) -> Vec<dap::StackFrame> {
#[cfg(test)]
pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
self.entries
.iter()
.flat_map(|frame| match frame {
@ -115,8 +115,8 @@ impl StackFrameList {
.unwrap_or_default()
}
#[cfg(any(test, feature = "test-support"))]
pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
#[cfg(test)]
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
self.stack_frames(cx)
.into_iter()
.map(|stack_frame| stack_frame.dap.clone())

View file

@ -540,8 +540,8 @@ impl VariableList {
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn assert_visual_entries(&self, expected: Vec<&str>) {
#[cfg(test)]
pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
const INDENT: &'static str = " ";
let entries = &self.entries;
@ -569,8 +569,8 @@ impl VariableList {
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn scopes(&self) -> Vec<dap::Scope> {
#[cfg(test)]
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
@ -582,8 +582,8 @@ impl VariableList {
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
#[cfg(test)]
pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
let mut idx = 0;
@ -604,8 +604,8 @@ impl VariableList {
}
#[track_caller]
#[cfg(any(test, feature = "test-support"))]
pub fn variables(&self) -> Vec<dap::Variable> {
#[cfg(test)]
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {

View file

@ -1,80 +0,0 @@
use std::time::Duration;
use anyhow::Result;
use dap::client::SessionId;
use gpui::{
Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task, Transformation,
percentage,
};
use project::debugger::session::Session;
use ui::{Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled, v_flex};
pub(crate) struct StartingState {
focus_handle: FocusHandle,
pub(super) session_id: SessionId,
_notify_parent: Task<()>,
}
pub(crate) enum StartingEvent {
Failed,
Finished(Entity<Session>),
}
impl EventEmitter<StartingEvent> for StartingState {}
impl StartingState {
pub(crate) fn new(
session_id: SessionId,
task: Task<Result<Entity<Session>>>,
cx: &mut Context<Self>,
) -> Self {
let _notify_parent = cx.spawn(async move |this, cx| {
let entity = task.await;
this.update(cx, |_, cx| {
if let Ok(entity) = entity {
cx.emit(StartingEvent::Finished(entity))
} else {
cx.emit(StartingEvent::Failed)
}
})
.ok();
});
Self {
session_id,
focus_handle: cx.focus_handle(),
_notify_parent,
}
}
}
impl Focusable for StartingState {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for StartingState {
fn render(
&mut self,
_window: &mut ui::Window,
_cx: &mut ui::Context<'_, Self>,
) -> impl ui::IntoElement {
v_flex()
.size_full()
.gap_1()
.items_center()
.child("Starting a debug adapter")
.child(
Icon::new(IconName::ArrowCircle)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
)
}
}

View file

@ -109,6 +109,7 @@ async fn test_show_attach_modal_and_select_process(
command: vec![],
},
],
true,
window,
cx,
)

View file

@ -99,8 +99,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
// we have one active session and one inert item
assert_eq!(2, this.pane().unwrap().read(cx).items_len());
// we have one active session
assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert!(running_state.read(cx).selected_thread_id().is_none());
});
})
@ -135,9 +135,9 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
.clone()
});
// we have one active session and one inert item
// we have one active session
assert_eq!(
2,
1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), running_state.read(cx).session_id());
@ -175,7 +175,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
assert_eq!(2, this.pane().unwrap().read(cx).items_len());
assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert_eq!(
ThreadId(1),
running_state.read(cx).selected_thread_id().unwrap()
@ -245,8 +245,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
// we have one active session and one inert item
assert_eq!(2, this.pane().unwrap().read(cx).items_len());
// we have one active session
assert_eq!(1, this.pane().unwrap().read(cx).items_len());
});
})
.unwrap();
@ -281,9 +281,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
.clone()
});
// we have one active session and one inert item
// we have one active session
assert_eq!(
2,
1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@ -323,9 +323,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
.clone()
});
// we have one active session and one inert item
// we have one active session
assert_eq!(
2,
1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@ -362,7 +362,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
assert_eq!(2, this.pane().unwrap().read(cx).items_len());
assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert_eq!(
ThreadId(1),
running_state.read(cx).selected_thread_id().unwrap()
@ -1447,7 +1447,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
})
.await;
cx.dispatch_action(workspace::ClearAllBreakpoints);
cx.dispatch_action(crate::ClearAllBreakpoints);
cx.run_until_parked();
let shutdown_session = project.update(cx, |project, cx| {

View file

@ -23,6 +23,7 @@ pub enum IconName {
ArrowCircle,
ArrowDown,
ArrowDownFromLine,
ArrowDownRight,
ArrowLeft,
ArrowRight,
ArrowRightLeft,
@ -44,6 +45,7 @@ pub enum IconName {
BookCopy,
BookPlus,
Brain,
BugOff,
CaseSensitive,
Check,
CheckDouble,
@ -55,6 +57,7 @@ pub enum IconName {
ChevronUp,
ChevronUpDown,
Circle,
CircleOff,
Clipboard,
Close,
Code,
@ -166,6 +169,7 @@ pub enum IconName {
Play,
Plus,
PocketKnife,
Power,
Public,
PullRequest,
Quote,

View file

@ -48,6 +48,7 @@ use worktree::Worktree;
pub enum DapStoreEvent {
DebugClientStarted(SessionId),
DebugSessionInitialized(SessionId),
DebugClientShutdown(SessionId),
DebugClientEvent {
session_id: SessionId,
@ -862,6 +863,10 @@ fn create_new_session(
}
}
this.update(cx, |_, cx| {
cx.emit(DapStoreEvent::DebugSessionInitialized(session_id));
})?;
Ok(session)
});
task

View file

@ -537,7 +537,11 @@ impl LocalMode {
Ok((adapter, binary))
}
pub fn initialize_sequence(
pub fn label(&self) -> String {
self.config.label.clone()
}
fn initialize_sequence(
&self,
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,

View file

@ -125,6 +125,22 @@ impl Inventory {
cx.new(|_| Self::default())
}
pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> {
self.templates_from_settings
.worktree
.values()
.flat_map(|tasks| {
tasks.iter().filter_map(|(kind, tasks)| {
if matches!(kind.1, TaskKind::Debug) {
Some(tasks)
} else {
None
}
})
})
.flatten()
.collect()
}
/// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, worktree tasks first, language tasks second
/// and global tasks last. No specific order inside source kinds groups.

View file

@ -41,7 +41,6 @@ impl TCPHost {
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
pub struct AttachConfig {
/// The processId to attach to, if left empty we will show a process picker
#[serde(default)]
pub process_id: Option<u32>,
}
@ -52,7 +51,8 @@ pub struct LaunchConfig {
pub program: String,
/// The current working directory of your project
pub cwd: Option<PathBuf>,
/// Args to pass to a debuggee
/// Arguments to pass to a debuggee
#[serde(default)]
pub args: Vec<String>,
}
@ -66,6 +66,17 @@ pub enum DebugRequestType {
Attach(AttachConfig),
}
impl From<LaunchConfig> for DebugRequestType {
fn from(launch_config: LaunchConfig) -> Self {
DebugRequestType::Launch(launch_config)
}
}
impl From<AttachConfig> for DebugRequestType {
fn from(attach_config: AttachConfig) -> Self {
DebugRequestType::Attach(attach_config)
}
}
/// Represents a request for starting the debugger.
/// Contrary to `DebugRequestType`, `DebugRequestDisposition` is not Serializable.
#[derive(PartialEq, Eq, Clone, Debug)]
@ -144,6 +155,37 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
}
}
impl TryFrom<TaskTemplate> for DebugTaskDefinition {
type Error = ();
fn try_from(value: TaskTemplate) -> Result<Self, Self::Error> {
let TaskType::Debug(debug_args) = value.task_type else {
return Err(());
};
let request = match debug_args.request {
crate::DebugArgsRequest::Launch => DebugRequestType::Launch(LaunchConfig {
program: value.command,
cwd: value.cwd.map(PathBuf::from),
args: value.args,
}),
crate::DebugArgsRequest::Attach(attach_config) => {
DebugRequestType::Attach(attach_config)
}
};
Ok(DebugTaskDefinition {
adapter: debug_args.adapter,
request,
label: value.label,
initialize_args: debug_args.initialize_args,
tcp_connection: debug_args.tcp_connection,
locator: debug_args.locator,
stop_on_entry: debug_args.stop_on_entry,
})
}
}
impl DebugTaskDefinition {
/// Translate from debug definition to a task template
pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
@ -249,3 +291,21 @@ impl TryFrom<DebugTaskFile> for TaskTemplates {
Ok(Self(templates))
}
}
#[cfg(test)]
mod tests {
use crate::{DebugRequestType, LaunchConfig};
#[test]
fn test_can_deserialize_non_attach_task() {
let deserialized: DebugRequestType =
serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
assert_eq!(
deserialized,
DebugRequestType::Launch(LaunchConfig {
program: "cafebabe".to_owned(),
..Default::default()
})
);
}
}

View file

@ -338,6 +338,7 @@ impl PickerDelegate for TasksModalDelegate {
debugger_ui::attach_modal::AttachModal::new(
project,
config.clone(),
true,
window,
cx,
)

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::Path;
use debugger_ui::Start;
use editor::Editor;
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
@ -8,7 +9,7 @@ use modal::{TaskOverrides, TasksModal};
use project::{Location, TaskContexts, Worktree};
use task::{RevealTarget, TaskContext, TaskId, TaskModal, TaskVariables, VariableName};
use workspace::tasks::schedule_task;
use workspace::{Start, Workspace, tasks::schedule_resolved_task};
use workspace::{Workspace, tasks::schedule_resolved_task};
mod modal;

View file

@ -71,6 +71,18 @@ impl SelectableButton for ToggleButton {
}
}
impl FixedWidth for ToggleButton {
fn width(mut self, width: DefiniteLength) -> Self {
self.base.width = Some(width);
self
}
fn full_width(mut self) -> Self {
self.base.width = Some(relative(1.));
self
}
}
impl Disableable for ToggleButton {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);

View file

@ -253,6 +253,7 @@ pub struct CheckboxWithLabel {
on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
filled: bool,
style: ToggleStyle,
checkbox_position: IconPosition,
}
// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
@ -271,6 +272,7 @@ impl CheckboxWithLabel {
on_click: Arc::new(on_click),
filled: false,
style: ToggleStyle::default(),
checkbox_position: IconPosition::Start,
}
}
@ -291,31 +293,51 @@ impl CheckboxWithLabel {
self.filled = true;
self
}
pub fn checkbox_position(mut self, position: IconPosition) -> Self {
self.checkbox_position = position;
self
}
}
impl RenderOnce for CheckboxWithLabel {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(
Checkbox::new(self.id.clone(), self.checked)
.style(self.style)
.when(self.filled, Checkbox::fill)
.on_click({
let on_click = self.on_click.clone();
move |checked, window, cx| {
(on_click)(checked, window, cx);
}
}),
)
.when(self.checkbox_position == IconPosition::Start, |this| {
this.child(
Checkbox::new(self.id.clone(), self.checked)
.style(self.style.clone())
.when(self.filled, Checkbox::fill)
.on_click({
let on_click = self.on_click.clone();
move |checked, window, cx| {
(on_click)(checked, window, cx);
}
}),
)
})
.child(
div()
.id(SharedString::from(format!("{}-label", self.id)))
.on_click(move |_event, window, cx| {
(self.on_click)(&self.checked.inverse(), window, cx);
.on_click({
let on_click = self.on_click.clone();
move |_event, window, cx| {
(on_click)(&self.checked.inverse(), window, cx);
}
})
.child(self.label),
)
.when(self.checkbox_position == IconPosition::End, |this| {
this.child(
Checkbox::new(self.id.clone(), self.checked)
.style(self.style)
.when(self.filled, Checkbox::fill)
.on_click(move |checked, window, cx| {
(self.on_click)(checked, window, cx);
}),
)
})
}
}

View file

@ -129,24 +129,6 @@ static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
actions!(assistant, [ShowConfiguration]);
actions!(
debugger,
[
Start,
Continue,
Disconnect,
Pause,
Restart,
StepInto,
StepOver,
StepOut,
StepBack,
Stop,
ToggleIgnoreBreakpoints,
ClearAllBreakpoints
]
);
actions!(
workspace,
[