debugger: Add console indicator and resolve debug configs from NewSessionModal (#28489)

The debug console will now show an indicator when it's unopened and
there's unread messages.

`NewSessionModal` attempts to resolve debug configurations before using
the config to start debugging. This allows users to use zed's task
variables in the modal prompt.

I had to invert tasks_ui dependency on debugger_ui so `NewSessionModal`
could get the correct `TaskContexts` by calling tasks_ui functions. A
consequence of this workspace has a new event `ShowAttachModal` that I'm
not a big fan of. @osiewicz if you have time could you please take a
look to see if there's a way around adding the event. I'm open to pair
on it too.

Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-04-10 18:29:03 -04:00 committed by GitHub
parent 66dd6726df
commit cf65d9437a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 204 additions and 82 deletions

View file

@ -1,13 +1,13 @@
[ [
{ {
"label": "Debug Zed with LLDB", "label": "Debug Zed (CodeLLDB)",
"adapter": "LLDB", "adapter": "CodeLLDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed", "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch", "request": "launch",
"cwd": "$ZED_WORKTREE_ROOT" "cwd": "$ZED_WORKTREE_ROOT"
}, },
{ {
"label": "Debug Zed with GDB", "label": "Debug Zed (GDB)",
"adapter": "GDB", "adapter": "GDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed", "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch", "request": "launch",

3
Cargo.lock generated
View file

@ -4232,6 +4232,7 @@ dependencies = [
"settings", "settings",
"sysinfo", "sysinfo",
"task", "task",
"tasks_ui",
"terminal_view", "terminal_view",
"theme", "theme",
"ui", "ui",
@ -14240,9 +14241,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"debugger_ui",
"editor", "editor",
"feature_flags",
"file_icons", "file_icons",
"fuzzy", "fuzzy",
"gpui", "gpui",

View file

@ -45,6 +45,7 @@ serde_json.workspace = true
settings.workspace = true settings.workspace = true
sysinfo.workspace = true sysinfo.workspace = true
task.workspace = true task.workspace = true
tasks_ui.workspace = true
terminal_view.workspace = true terminal_view.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true

View file

@ -77,8 +77,45 @@ impl DebugPanel {
let project = workspace.project().clone(); let project = workspace.project().clone();
let dap_store = project.read(cx).dap_store(); let dap_store = project.read(cx).dap_store();
let _subscriptions = let weak = cx.weak_entity();
vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
let modal_subscription =
cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
let modal_entity = cx.entity();
weak.update(cx, |_: &mut DebugPanel, cx| {
let Some(window) = window else {
log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
return;
};
cx.subscribe_in(
&modal_entity,
window,
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
panel.workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
crate::attach_modal::AttachModal::new(
project,
event.debug_config.clone(),
true,
window,
cx,
)
});
}).ok();
},
)
.detach();
})
.ok();
});
let _subscriptions = vec![
cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
modal_subscription,
];
let debug_panel = Self { let debug_panel = Self {
size: px(300.), size: px(300.),

View file

@ -156,7 +156,17 @@ pub fn init(cx: &mut App) {
}); });
} }
}, },
); )
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
tasks_ui::toggle_modal(
workspace,
None,
task::TaskModal::DebugModal,
window,
cx,
)
.detach();
});
}) })
}) })
.detach(); .detach();
@ -237,7 +247,7 @@ pub fn init(cx: &mut App) {
state.session().update(cx, |session, cx| { state.session().update(cx, |session, cx| {
session.evaluate(text, None, stack_id, None, cx); session.evaluate(text, None, stack_id, None, cx);
}) });
}); });
Some(()) Some(())
}); });

View file

@ -102,7 +102,8 @@ impl NewSessionModal {
}, },
}) })
} }
fn start_new_session(&self, cx: &mut Context<Self>) -> Result<()> {
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let config = self let config = self
.debug_config(cx) .debug_config(cx)
@ -112,10 +113,41 @@ impl NewSessionModal {
panel.past_debug_definition = Some(config.clone()); panel.past_debug_definition = Some(config.clone());
}); });
let task_contexts = workspace
.update(cx, |workspace, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})
.ok();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let task_context = if let Some(task) = task_contexts {
task.await
.active_worktree_context
.map_or(task::TaskContext::default(), |context| context.1)
} else {
task::TaskContext::default()
};
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
let task =
project.update(cx, |this, cx| this.start_debug_session(config.into(), cx))?; let task = project.update(cx, |this, cx| {
if let Some(debug_config) =
config
.clone()
.to_zed_format()
.ok()
.and_then(|task_template| {
task_template
.resolve_task("debug_task", &task_context)
.and_then(|resolved_task| {
resolved_task.resolved_debug_adapter_config()
})
})
{
this.start_debug_session(debug_config, cx)
} else {
this.start_debug_session(config.into(), cx)
}
})?;
let spawn_result = task.await; let spawn_result = task.await;
if spawn_result.is_ok() { if spawn_result.is_ok() {
this.update(cx, |_, cx| { this.update(cx, |_, cx| {
@ -614,8 +646,8 @@ impl Render for NewSessionModal {
}) })
.child( .child(
Button::new("debugger-spawn", "Start") Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, _, cx| { .on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(cx).log_err(); this.start_new_session(window, cx).log_err();
})) }))
.disabled(self.debugger.is_none()), .disabled(self.debugger.is_none()),
), ),

View file

@ -26,8 +26,8 @@ use rpc::proto::ViewId;
use settings::Settings; use settings::Settings;
use stack_frame_list::StackFrameList; use stack_frame_list::StackFrameList;
use ui::{ use ui::{
App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, ParentElement, AnyElement, App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label,
Render, SharedString, Styled, Window, div, h_flex, v_flex, LabelCommon as _, ParentElement, Render, SharedString, Styled, Window, div, h_flex, v_flex,
}; };
use util::ResultExt; use util::ResultExt;
use variable_list::VariableList; use variable_list::VariableList;
@ -86,6 +86,7 @@ struct SubView {
inner: AnyView, inner: AnyView,
pane_focus_handle: FocusHandle, pane_focus_handle: FocusHandle,
tab_name: SharedString, tab_name: SharedString,
show_indicator: Box<dyn Fn(&App) -> bool>,
} }
impl SubView { impl SubView {
@ -93,12 +94,14 @@ impl SubView {
pane_focus_handle: FocusHandle, pane_focus_handle: FocusHandle,
view: AnyView, view: AnyView,
tab_name: SharedString, tab_name: SharedString,
show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
cx.new(|_| Self { cx.new(|_| Self {
tab_name, tab_name,
inner: view, inner: view,
pane_focus_handle, pane_focus_handle,
show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
}) })
} }
} }
@ -110,8 +113,27 @@ impl Focusable for SubView {
impl EventEmitter<()> for SubView {} impl EventEmitter<()> for SubView {}
impl Item for SubView { impl Item for SubView {
type Event = (); type Event = ();
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some(self.tab_name.clone()) fn tab_content(
&self,
params: workspace::item::TabContentParams,
_: &Window,
cx: &App,
) -> AnyElement {
let label = Label::new(self.tab_name.clone())
.color(params.text_color())
.into_any_element();
if !params.selected && self.show_indicator.as_ref()(cx) {
return h_flex()
.justify_between()
.child(ui::Indicator::dot())
.gap_2()
.child(label)
.into_any_element();
}
label
} }
} }
@ -315,6 +337,7 @@ impl RunningState {
this.focus_handle(cx), this.focus_handle(cx),
stack_frame_list.clone().into(), stack_frame_list.clone().into(),
SharedString::new_static("Frames"), SharedString::new_static("Frames"),
None,
cx, cx,
)), )),
true, true,
@ -329,6 +352,7 @@ impl RunningState {
breakpoints.focus_handle(cx), breakpoints.focus_handle(cx),
breakpoints.into(), breakpoints.into(),
SharedString::new_static("Breakpoints"), SharedString::new_static("Breakpoints"),
None,
cx, cx,
)), )),
true, true,
@ -346,6 +370,7 @@ impl RunningState {
variable_list.focus_handle(cx), variable_list.focus_handle(cx),
variable_list.clone().into(), variable_list.clone().into(),
SharedString::new_static("Variables"), SharedString::new_static("Variables"),
None,
cx, cx,
)), )),
true, true,
@ -359,6 +384,7 @@ impl RunningState {
this.focus_handle(cx), this.focus_handle(cx),
module_list.clone().into(), module_list.clone().into(),
SharedString::new_static("Modules"), SharedString::new_static("Modules"),
None,
cx, cx,
)), )),
false, false,
@ -371,11 +397,17 @@ impl RunningState {
}); });
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
rightmost_pane.update(cx, |this, cx| { rightmost_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item( this.add_item(
Box::new(SubView::new( Box::new(SubView::new(
this.focus_handle(cx), this.focus_handle(cx),
console.clone().into(), console.clone().into(),
SharedString::new_static("Console"), SharedString::new_static("Console"),
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx, cx,
)), )),
true, true,

View file

@ -105,6 +105,10 @@ impl Console {
} }
} }
pub(crate) fn show_indicator(&self, cx: &App) -> bool {
self.session.read(cx).has_new_output(self.last_token)
}
pub fn add_messages<'a>( pub fn add_messages<'a>(
&mut self, &mut self,
events: impl Iterator<Item = &'a OutputEvent>, events: impl Iterator<Item = &'a OutputEvent>,

View file

@ -1152,6 +1152,10 @@ impl Session {
} }
} }
pub fn has_new_output(&self, last_update: OutputToken) -> bool {
self.output_token.0.checked_sub(last_update.0).unwrap_or(0) != 0
}
pub fn output( pub fn output(
&self, &self,
since: OutputToken, since: OutputToken,

View file

@ -14,11 +14,9 @@ path = "src/tasks_ui.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
collections.workspace = true collections.workspace = true
debugger_ui.workspace = true
editor.workspace = true editor.workspace = true
file_icons.workspace = true file_icons.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
feature_flags.workspace = true
itertools.workspace = true itertools.workspace = true
gpui.workspace = true gpui.workspace = true
menu.workspace = true menu.workspace = true

View file

@ -128,9 +128,9 @@ impl TasksModalDelegate {
} }
} }
pub(crate) struct TasksModal { pub struct TasksModal {
picker: Entity<Picker<TasksModalDelegate>>, picker: Entity<Picker<TasksModalDelegate>>,
_subscription: Subscription, _subscription: [Subscription; 2],
} }
impl TasksModal { impl TasksModal {
@ -156,9 +156,16 @@ impl TasksModal {
cx, cx,
) )
}); });
let _subscription = cx.subscribe(&picker, |_, _, _, cx| { let _subscription = [
cx.emit(DismissEvent); cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
}); cx.emit(DismissEvent);
}),
cx.subscribe(&picker, |_, _, event: &ShowAttachModal, cx| {
cx.emit(ShowAttachModal {
debug_config: event.debug_config.clone(),
});
}),
];
Self { Self {
picker, picker,
_subscription, _subscription,
@ -179,7 +186,13 @@ impl Render for TasksModal {
} }
} }
pub struct ShowAttachModal {
pub debug_config: DebugTaskDefinition,
}
impl EventEmitter<DismissEvent> for TasksModal {} impl EventEmitter<DismissEvent> for TasksModal {}
impl EventEmitter<ShowAttachModal> for TasksModal {}
impl EventEmitter<ShowAttachModal> for Picker<TasksModalDelegate> {}
impl Focusable for TasksModal { impl Focusable for TasksModal {
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
@ -321,7 +334,7 @@ impl PickerDelegate for TasksModalDelegate {
fn confirm( fn confirm(
&mut self, &mut self,
omit_history_entry: bool, omit_history_entry: bool,
window: &mut Window, _: &mut Window,
cx: &mut Context<picker::Picker<Self>>, cx: &mut Context<picker::Picker<Self>>,
) { ) {
let current_match_index = self.selected_index(); let current_match_index = self.selected_index();
@ -346,51 +359,52 @@ impl PickerDelegate for TasksModalDelegate {
} }
} }
self.workspace match task.task_type() {
.update(cx, |workspace, cx| { TaskType::Debug(config) if config.locator.is_none() => {
match task.task_type() { let Some(config): Option<DebugTaskDefinition> = task
TaskType::Debug(config) if config.locator.is_none() => { .resolved_debug_adapter_config()
let Some(config): Option<DebugTaskDefinition> = task .and_then(|config| config.try_into().ok())
.resolved_debug_adapter_config() else {
.and_then(|config| config.try_into().ok()) return;
else { };
return;
};
let project = workspace.project().clone();
match &config.request { match &config.request {
DebugRequestType::Attach(attach_config) DebugRequestType::Attach(attach_config)
if attach_config.process_id.is_none() => if attach_config.process_id.is_none() =>
{ {
workspace.toggle_modal(window, cx, |window, cx| { cx.emit(ShowAttachModal {
debugger_ui::attach_modal::AttachModal::new( debug_config: config.clone(),
project, });
config.clone(), return;
true, }
window, _ => {
cx, self.workspace
) .update(cx, |workspace, cx| {
}); workspace.project().update(cx, |project, cx| {
}
_ => {
project.update(cx, |project, cx| {
project project
.start_debug_session(config.into(), cx) .start_debug_session(config.into(), cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
}); });
} })
} .ok();
} }
_ => schedule_resolved_task( }
workspace, }
task_source_kind, _ => {
task, self.workspace
omit_history_entry, .update(cx, |workspace, cx| {
cx, schedule_resolved_task(
), workspace,
}; task_source_kind,
}) task,
.ok(); omit_history_entry,
cx,
);
})
.ok();
}
};
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }

View file

@ -1,11 +1,9 @@
use std::path::Path; use std::path::Path;
use collections::HashMap; use collections::HashMap;
use debugger_ui::Start;
use editor::Editor; use editor::Editor;
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::{App, AppContext as _, Context, Entity, Task, Window}; use gpui::{App, AppContext as _, Context, Entity, Task, Window};
use modal::{TaskOverrides, TasksModal}; use modal::TaskOverrides;
use project::{Location, TaskContexts, TaskSourceKind, Worktree}; use project::{Location, TaskContexts, TaskSourceKind, Worktree};
use task::{ use task::{
RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName, RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName,
@ -15,11 +13,11 @@ use workspace::{Workspace, tasks::schedule_resolved_task};
mod modal; mod modal;
pub use modal::{Rerun, Spawn}; pub use modal::{Rerun, ShowAttachModal, Spawn, TasksModal};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.observe_new( cx.observe_new(
|workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>| { |workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context<Workspace>| {
workspace workspace
.register_action(spawn_task_or_modal) .register_action(spawn_task_or_modal)
.register_action(move |workspace, action: &modal::Rerun, window, cx| { .register_action(move |workspace, action: &modal::Rerun, window, cx| {
@ -89,17 +87,6 @@ pub fn init(cx: &mut App) {
toggle_modal(workspace, None, TaskModal::ScriptModal, window, cx).detach(); toggle_modal(workspace, None, TaskModal::ScriptModal, window, cx).detach();
}; };
}); });
let Some(window) = window else {
return;
};
cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| {
workspace.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
crate::toggle_modal(workspace, None, task::TaskModal::DebugModal, window, cx)
.detach();
});
});
}, },
) )
.detach(); .detach();
@ -277,7 +264,11 @@ where
}) })
} }
fn task_contexts(workspace: &Workspace, window: &mut Window, cx: &mut App) -> Task<TaskContexts> { pub fn task_contexts(
workspace: &Workspace,
window: &mut Window,
cx: &mut App,
) -> Task<TaskContexts> {
let active_item = workspace.active_item(cx); let active_item = workspace.active_item(cx);
let active_worktree = active_item let active_worktree = active_item
.as_ref() .as_ref()