pub(crate) mod breakpoint_list; pub(crate) mod console; pub(crate) mod loaded_source_list; pub(crate) mod module_list; pub mod stack_frame_list; pub mod variable_list; use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::{ ToggleExpandItem, new_process_modal::resolve_path, persistence::{self, DebuggerPaneItem, SerializedLayout}, }; use super::DebugPanelItemEvent; use anyhow::{Context as _, Result, anyhow}; use breakpoint_list::BreakpointList; use collections::{HashMap, IndexMap}; use console::Console; use dap::{ Capabilities, DapRegistry, RunInTerminalRequestArguments, Thread, adapters::{DebugAdapterName, DebugTaskDefinition}, client::SessionId, debugger_settings::DebuggerSettings, }; use futures::{SinkExt, channel::mpsc}; use gpui::{ Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable, NoAction, Pixels, Point, Subscription, Task, WeakEntity, }; use language::Buffer; use loaded_source_list::LoadedSourceList; use module_list::ModuleList; use project::{ DebugScenarioContext, Project, WorktreeId, debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}, terminals::TerminalKind, }; use rpc::proto::ViewId; use serde_json::Value; use settings::Settings; use stack_frame_list::StackFrameList; use task::{ BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig, substitute_variables_in_str, }; use terminal_view::TerminalView; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex, }; use util::ResultExt; use variable_list::VariableList; use workspace::{ ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, SplitDirection, Workspace, item::TabContentParams, move_item, pane::Event, }; pub struct RunningState { session: Entity, thread_id: Option, focus_handle: FocusHandle, _remote_id: Option, workspace: WeakEntity, session_id: SessionId, variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, loaded_sources_list: Entity, pub debug_terminal: Entity, module_list: Entity, console: Entity, breakpoint_list: Entity, panes: PaneGroup, active_pane: Entity, pane_close_subscriptions: HashMap, dock_axis: Axis, _schedule_serialize: Option>, pub(crate) scenario: Option, pub(crate) scenario_context: Option, } impl RunningState { pub(crate) fn thread_id(&self) -> Option { self.thread_id } pub(crate) fn active_pane(&self) -> &Entity { &self.active_pane } } impl Render for RunningState { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let zoomed_pane = self .panes .panes() .into_iter() .find(|pane| pane.read(cx).is_zoomed()); let active = self.panes.panes().into_iter().next(); let pane = if let Some(ref zoomed_pane) = zoomed_pane { zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element()) } else if let Some(active) = active { self.panes .render( None, &ActivePaneDecorator::new(active, &self.workspace), window, cx, ) .into_any_element() } else { div().into_any_element() }; let thread_status = self .thread_id .map(|thread_id| self.session.read(cx).thread_status(thread_id)) .unwrap_or(ThreadStatus::Exited); self.variable_list.update(cx, |this, cx| { this.disabled(thread_status != ThreadStatus::Stopped, cx); }); v_flex() .size_full() .key_context("DebugSessionItem") .track_focus(&self.focus_handle(cx)) .child(h_flex().flex_1().child(pane)) } } pub(crate) struct SubView { inner: AnyView, item_focus_handle: FocusHandle, kind: DebuggerPaneItem, show_indicator: Box bool>, actions: Option AnyElement>>, hovered: bool, } impl SubView { pub(crate) fn new( item_focus_handle: FocusHandle, view: AnyView, kind: DebuggerPaneItem, cx: &mut App, ) -> Entity { cx.new(|_| Self { kind, inner: view, item_focus_handle, show_indicator: Box::new(|_| false), actions: None, hovered: false, }) } pub(crate) fn console(console: Entity, cx: &mut App) -> Entity { let weak_console = console.downgrade(); let this = Self::new( console.focus_handle(cx), console.into(), DebuggerPaneItem::Console, cx, ); this.update(cx, |this, _| { this.with_indicator(Box::new(move |cx| { weak_console .read_with(cx, |console, cx| console.show_indicator(cx)) .unwrap_or_default() })) }); this } pub(crate) fn breakpoint_list(list: Entity, cx: &mut App) -> Entity { let weak_list = list.downgrade(); let focus_handle = list.focus_handle(cx); let this = Self::new( focus_handle.clone(), list.into(), DebuggerPaneItem::BreakpointList, cx, ); this.update(cx, |this, _| { this.with_actions(Box::new(move |_, cx| { weak_list .update(cx, |this, _| this.render_control_strip()) .unwrap_or_else(|_| div().into_any_element()) })); }); this } pub(crate) fn view_kind(&self) -> DebuggerPaneItem { self.kind } pub(crate) fn with_indicator(&mut self, indicator: Box bool>) { self.show_indicator = indicator; } pub(crate) fn with_actions( &mut self, actions: Box AnyElement>, ) { self.actions = Some(actions); } } impl Focusable for SubView { fn focus_handle(&self, _: &App) -> FocusHandle { self.item_focus_handle.clone() } } impl EventEmitter<()> for SubView {} impl Item for SubView { type Event = (); /// This is used to serialize debugger pane layouts /// A SharedString gets converted to a enum and back during serialization/deserialization. fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { self.kind.to_shared_string() } fn tab_tooltip_text(&self, _: &App) -> Option { Some(self.kind.tab_tooltip()) } fn tab_content( &self, params: workspace::item::TabContentParams, _: &Window, cx: &App, ) -> AnyElement { let label = Label::new(self.kind.to_shared_string()) .size(ui::LabelSize::Small) .color(params.text_color()) .line_height_style(ui::LineHeightStyle::UiLabel); 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.into_any_element() } } impl Render for SubView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .id(SharedString::from(format!( "subview-container-{}", self.kind.to_shared_string() ))) .on_hover(cx.listener(|this, hovered, _, cx| { this.hovered = *hovered; cx.notify(); })) .size_full() // Add border unconditionally to prevent layout shifts on focus changes. .border_1() .when(self.item_focus_handle.contains_focused(window, cx), |el| { el.border_color(cx.theme().colors().pane_focused_border) }) .child(self.inner.clone()) } } pub(crate) fn new_debugger_pane( workspace: WeakEntity, project: Entity, window: &mut Window, cx: &mut Context, ) -> Entity { let weak_running = cx.weak_entity(); let custom_drop_handle = { let workspace = workspace.clone(); let project = project.downgrade(); let weak_running = weak_running.clone(); move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context| { let Some(tab) = any.downcast_ref::() else { return ControlFlow::Break(()); }; let Some(project) = project.upgrade() else { return ControlFlow::Break(()); }; let this_pane = cx.entity().clone(); let item = if tab.pane == this_pane { pane.item_for_index(tab.ix) } else { tab.pane.read(cx).item_for_index(tab.ix) }; let Some(item) = item.filter(|item| item.downcast::().is_some()) else { return ControlFlow::Break(()); }; let source = tab.pane.clone(); let item_id_to_move = item.item_id(); let Ok(new_split_pane) = pane .drag_split_direction() .map(|split_direction| { weak_running.update(cx, |running, cx| { let new_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); let _previous_subscription = running.pane_close_subscriptions.insert( new_pane.entity_id(), cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event), ); debug_assert!(_previous_subscription.is_none()); running .panes .split(&this_pane, &new_pane, split_direction)?; anyhow::Ok(new_pane) }) }) .transpose() else { return ControlFlow::Break(()); }; match new_split_pane.transpose() { // Source pane may be the one currently updated, so defer the move. Ok(Some(new_pane)) => cx .spawn_in(window, async move |_, cx| { cx.update(|window, cx| { move_item( &source, &new_pane, item_id_to_move, new_pane.read(cx).active_item_index(), true, window, cx, ); }) .ok(); }) .detach(), // If we drop into existing pane or current pane, // regular pane drop handler will take care of it, // using the right tab index for the operation. Ok(None) => return ControlFlow::Continue(()), err @ Err(_) => { err.log_err(); return ControlFlow::Break(()); } }; ControlFlow::Break(()) } }; let ret = cx.new(move |cx| { let mut pane = Pane::new( workspace.clone(), project.clone(), Default::default(), None, NoAction.boxed_clone(), window, cx, ); let focus_handle = pane.focus_handle(cx); pane.set_can_split(Some(Arc::new({ let weak_running = weak_running.clone(); move |pane, dragged_item, _window, cx| { if let Some(tab) = dragged_item.downcast_ref::() { let is_current_pane = tab.pane == cx.entity(); let Some(can_drag_away) = weak_running .read_with(cx, |running_state, _| { let current_panes = running_state.panes.panes(); !current_panes.contains(&&tab.pane) || current_panes.len() > 1 || (!is_current_pane || pane.items_len() > 1) }) .ok() else { return false; }; if can_drag_away { let item = if is_current_pane { pane.item_for_index(tab.ix) } else { tab.pane.read(cx).item_for_index(tab.ix) }; if let Some(item) = item { return item.downcast::().is_some(); } } } false } }))); pane.set_can_toggle_zoom(false, cx); pane.display_nav_history_buttons(None); pane.set_custom_drop_handle(cx, custom_drop_handle); pane.set_should_display_tab_bar(|_, _| true); pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None)); pane.set_render_tab_bar(cx, { move |pane, window, cx| { let active_pane_item = pane.active_item(); let pane_group_id: SharedString = format!("pane-zoom-button-hover-{}", cx.entity_id()).into(); let as_subview = active_pane_item .as_ref() .and_then(|item| item.downcast::()); let is_hovered = as_subview .as_ref() .map_or(false, |item| item.read(cx).hovered); h_flex() .group(pane_group_id.clone()) .justify_between() .bg(cx.theme().colors().tab_bar_background) .border_b_1() .px_2() .border_color(cx.theme().colors().border) .track_focus(&focus_handle) .on_action(|_: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { return; } else { cx.propagate(); } }) .child( h_flex() .w_full() .gap_1() .h(Tab::container_height(cx)) .drag_over::(|bar, _, _, cx| { bar.bg(cx.theme().colors().drop_target_background) }) .on_drop(cx.listener( move |this, dragged_tab: &DraggedTab, window, cx| { this.drag_split_direction = None; this.handle_tab_drop(dragged_tab, this.items_len(), window, cx) }, )) .children(pane.items().enumerate().map(|(ix, item)| { let selected = active_pane_item .as_ref() .map_or(false, |active| active.item_id() == item.item_id()); let deemphasized = !pane.has_focus(window, cx); let item_ = item.boxed_clone(); div() .id(SharedString::from(format!( "debugger_tab_{}", item.item_id().as_u64() ))) .p_1() .rounded_md() .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |this, tooltip| { this.tooltip(Tooltip::text(tooltip)) }) .map(|this| { let theme = cx.theme(); if selected { let color = theme.colors().tab_active_background; let color = if deemphasized { color.opacity(0.5) } else { color }; this.bg(color) } else { let hover_color = theme.colors().element_hover; this.hover(|style| style.bg(hover_color)) } }) .on_click(cx.listener(move |this, _, window, cx| { let index = this.index_for_item(&*item_); if let Some(index) = index { this.activate_item(index, true, true, window, cx); } })) .child(item.tab_content( TabContentParams { selected, deemphasized, ..Default::default() }, window, cx, )) .on_drop(cx.listener( move |this, dragged_tab: &DraggedTab, window, cx| { this.drag_split_direction = None; this.handle_tab_drop(dragged_tab, ix, window, cx) }, )) .on_drag( DraggedTab { item: item.boxed_clone(), pane: cx.entity().clone(), detail: 0, is_active: selected, ix, }, |tab, _, _, cx| cx.new(|_| tab.clone()), ) })), ) .child({ let zoomed = pane.is_zoomed(); h_flex() .visible_on_hover(pane_group_id) .when(is_hovered, |this| this.visible()) .when_some(as_subview.as_ref(), |this, subview| { subview.update(cx, |view, cx| { let Some(additional_actions) = view.actions.as_mut() else { return this; }; this.child(additional_actions(window, cx)) }) }) .child( IconButton::new( SharedString::from(format!( "debug-toggle-zoom-{}", cx.entity_id() )), if zoomed { IconName::Minimize } else { IconName::Maximize }, ) .icon_size(IconSize::XSmall) .on_click(cx.listener(move |pane, _, _, cx| { let is_zoomed = pane.is_zoomed(); pane.set_zoomed(!is_zoomed, cx); cx.notify(); })) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { let zoomed_text = if zoomed { "Minimize" } else { "Expand" }; Tooltip::for_action_in( zoomed_text, &ToggleExpandItem, &focus_handle, window, cx, ) } }), ) }) .into_any_element() } }); pane }); ret } pub struct DebugTerminal { pub terminal: Option>, focus_handle: FocusHandle, _subscriptions: [Subscription; 1], } impl DebugTerminal { fn empty(window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| { if let Some(terminal) = this.terminal.as_ref() { terminal.focus_handle(cx).focus(window); } }); Self { terminal: None, focus_handle, _subscriptions: [focus_subscription], } } } impl gpui::Render for DebugTerminal { fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { div() .size_full() .track_focus(&self.focus_handle) .children(self.terminal.clone()) } } impl Focusable for DebugTerminal { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl RunningState { // todo(debugger) move this to util and make it so you pass a closure to it that converts a string pub(crate) fn substitute_variables_in_config( config: &mut serde_json::Value, context: &TaskContext, ) { match config { serde_json::Value::Object(obj) => { obj.values_mut() .for_each(|value| Self::substitute_variables_in_config(value, context)); } serde_json::Value::Array(array) => { array .iter_mut() .for_each(|value| Self::substitute_variables_in_config(value, context)); } serde_json::Value::String(s) => { // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces. if s.starts_with("\"$ZED_") && s.ends_with('"') { *s = s[1..s.len() - 1].to_string(); } if let Some(substituted) = substitute_variables_in_str(&s, context) { *s = substituted; } } _ => {} } } pub(crate) fn relativize_paths( key: Option<&str>, config: &mut serde_json::Value, context: &TaskContext, ) { match config { serde_json::Value::Object(obj) => { obj.iter_mut() .for_each(|(key, value)| Self::relativize_paths(Some(key), value, context)); } serde_json::Value::Array(array) => { array .iter_mut() .for_each(|value| Self::relativize_paths(None, value, context)); } serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => { // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces. if s.starts_with("\"$ZED_") && s.ends_with('"') { *s = s[1..s.len() - 1].to_string(); } resolve_path(s); if let Some(substituted) = substitute_variables_in_str(&s, context) { *s = substituted; } } _ => {} } } pub(crate) fn new( session: Entity, project: Entity, workspace: WeakEntity, parent_terminal: Option>, serialized_pane_layout: Option, dock_axis: Axis, window: &mut Window, cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); let session_id = session.read(cx).session_id(); let weak_state = cx.weak_entity(); let stack_frame_list = cx.new(|cx| { StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx) }); let debug_terminal = parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx))); let variable_list = cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx)); let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); let console = cx.new(|cx| { Console::new( session.clone(), stack_frame_list.clone(), variable_list.clone(), window, cx, ) }); let breakpoint_list = BreakpointList::new( Some(session.clone()), workspace.clone(), &project, window, cx, ); let _subscriptions = vec![ cx.on_app_quit(move |this, cx| { let shutdown = this .session .update(cx, |session, cx| session.on_app_quit(cx)); let terminal = this.debug_terminal.clone(); async move { shutdown.await; drop(terminal) } }), cx.observe(&module_list, |_, _, cx| cx.notify()), cx.subscribe_in(&session, window, |this, _, event, window, cx| { match event { SessionEvent::Stopped(thread_id) => { let panel = this .workspace .update(cx, |workspace, cx| { workspace.open_panel::(window, cx); workspace.panel::(cx) }) .log_err() .flatten(); if let Some(thread_id) = thread_id { this.select_thread(*thread_id, window, cx); } if let Some(panel) = panel { let id = this.session_id; window.defer(cx, move |window, cx| { panel.update(cx, |this, cx| { this.activate_session_by_id(id, window, cx); }) }) } } SessionEvent::Threads => { let threads = this.session.update(cx, |this, cx| this.threads(cx)); this.select_current_thread(&threads, window, cx); } SessionEvent::CapabilitiesLoaded => { let capabilities = this.capabilities(cx); if !capabilities.supports_modules_request.unwrap_or(false) { this.remove_pane_item(DebuggerPaneItem::Modules, window, cx); } if !capabilities .supports_loaded_sources_request .unwrap_or(false) { this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx); } } SessionEvent::RunInTerminal { request, sender } => this .handle_run_in_terminal(request, sender.clone(), window, cx) .detach_and_log_err(cx), _ => {} } cx.notify() }), cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.serialize_layout(window, cx); }), ]; let mut pane_close_subscriptions = HashMap::default(); let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| { persistence::deserialize_pane_layout( serialized_layout.panes, dock_axis != serialized_layout.dock_axis, &workspace, &project, &stack_frame_list, &variable_list, &module_list, &console, &breakpoint_list, &loaded_source_list, &debug_terminal, &mut pane_close_subscriptions, window, cx, ) }) { workspace::PaneGroup::with_root(root) } else { pane_close_subscriptions.clear(); let root = Self::default_pane_layout( project, &workspace, &stack_frame_list, &variable_list, &console, &breakpoint_list, &debug_terminal, dock_axis, &mut pane_close_subscriptions, window, cx, ); workspace::PaneGroup::with_root(root) }; let active_pane = panes.first_pane(); Self { session, workspace, focus_handle, variable_list, _subscriptions, thread_id: None, _remote_id: None, stack_frame_list, session_id, panes, active_pane, module_list, console, breakpoint_list, loaded_sources_list: loaded_source_list, pane_close_subscriptions, debug_terminal, dock_axis, _schedule_serialize: None, scenario: None, scenario_context: None, } } pub(crate) fn remove_pane_item( &mut self, item_kind: DebuggerPaneItem, window: &mut Window, cx: &mut Context, ) { if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| { Some(pane).zip( pane.read(cx) .items() .find(|item| { item.act_as::(cx) .is_some_and(|view| view.read(cx).kind == item_kind) }) .map(|item| item.item_id()), ) }) { pane.update(cx, |pane, cx| { pane.remove_item(item_id, false, true, window, cx) }) } } pub(crate) fn has_pane_at_position(&self, position: Point) -> bool { self.panes.pane_at_pixel_position(position).is_some() } pub(crate) fn resolve_scenario( &self, scenario: DebugScenario, task_context: TaskContext, buffer: Option>, worktree_id: Option, window: &Window, cx: &mut Context, ) -> Task> { let Some(workspace) = self.workspace.upgrade() else { return Task::ready(Err(anyhow!("no workspace"))); }; let project = workspace.read(cx).project().clone(); let dap_store = project.read(cx).dap_store().downgrade(); let dap_registry = cx.global::().clone(); let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); let is_local = project.read(cx).is_local(); cx.spawn_in(window, async move |this, cx| { let DebugScenario { adapter, label, build, mut config, tcp_connection, } = scenario; Self::relativize_paths(None, &mut config, &task_context); Self::substitute_variables_in_config(&mut config, &task_context); let request_type = match dap_registry .adapter(&adapter) .with_context(|| format!("{}: is not a valid adapter name", &adapter)) { Ok(adapter) => adapter.request_kind(&config).await, Err(e) => Err(e) }; let config_is_valid = request_type.is_ok(); let mut extra_config = Value::Null; let build_output = if let Some(build) = build { let (task_template, locator_name) = match build { BuildTaskDefinition::Template { task_template, locator_name, } => (task_template, locator_name), BuildTaskDefinition::ByName(ref label) => { let task = task_store.update(cx, |this, cx| { this.task_inventory().map(|inventory| { inventory.read(cx).task_template_by_label( buffer, worktree_id, &label, cx, ) }) })?; let task = match task { Some(task) => task.await, None => None, }.with_context(|| format!("Couldn't find task template for {build:?}"))?; (task, None) } }; let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else { anyhow::bail!("Could not resolve task variables within a debug scenario"); }; let locator_name = if let Some(locator_name) = locator_name { extra_config = config.clone(); debug_assert!(!config_is_valid); Some(locator_name) } else if !config_is_valid { let task = dap_store .update(cx, |this, cx| { this.debug_scenario_for_build_task( task.original_task().clone(), adapter.clone().into(), task.display_label().to_owned().into(), cx, ) }); if let Ok(t) = task { t.await.and_then(|scenario| { extra_config = scenario.config; match scenario.build { Some(BuildTaskDefinition::Template { locator_name, .. }) => locator_name, _ => None, } }) } else { None } } else { None }; let builder = ShellBuilder::new(is_local, &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); let task_with_shell = SpawnInTerminal { command_label, command: Some(command), args, ..task.resolved.clone() }; let terminal = project .update_in(cx, |project, window, cx| { project.create_terminal( TerminalKind::Task(task_with_shell.clone()), window.window_handle(), cx, ) })? .await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new( terminal.clone(), weak_workspace, None, weak_project, window, cx, ) })?; this.update_in(cx, |this, window, cx| { this.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx); this.debug_terminal.update(cx, |debug_terminal, cx| { debug_terminal.terminal = Some(terminal_view); cx.notify(); }); })?; let exit_status = terminal .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? .await .context("Failed to wait for completed task")?; if !exit_status.success() { anyhow::bail!("Build failed"); } Some((task.resolved.clone(), locator_name, extra_config)) } else { None }; if config_is_valid { } else if let Some((task, locator_name, extra_config)) = build_output { let locator_name = locator_name.with_context(|| { format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err() .map(|err| err.to_string()) .unwrap_or_default()) })?; let request = dap_store .update(cx, |this, cx| { this.run_debug_locator(&locator_name, task, cx) })? .await?; let zed_config = ZedDebugConfig { label: label.clone(), adapter: adapter.clone(), request, stop_on_entry: None, }; let scenario = dap_registry .adapter(&adapter) .with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config) .await?; config = scenario.config; util::merge_non_null_json_value_into(extra_config, &mut config); Self::substitute_variables_in_config(&mut config, &task_context); } else { let Err(e) = request_type else { unreachable!(); }; anyhow::bail!("Zed cannot determine how to run this debug scenario. `build` field was not provided and Debug Adapter won't accept provided configuration because: {e}"); }; Ok(DebugTaskDefinition { label, adapter: DebugAdapterName(adapter), config, tcp_connection, }) }) } fn handle_run_in_terminal( &self, request: &RunInTerminalRequestArguments, mut sender: mpsc::Sender>, window: &mut Window, cx: &mut Context, ) -> Task> { let running = cx.entity(); let Ok(project) = self .workspace .read_with(cx, |workspace, _| workspace.project().clone()) else { return Task::ready(Err(anyhow!("no workspace"))); }; let session = self.session.read(cx); let cwd = Some(&request.cwd) .filter(|cwd| cwd.len() > 0) .map(PathBuf::from) .or_else(|| session.binary().unwrap().cwd.clone()); let mut envs: HashMap = self.session.read(cx).task_context().project_env.clone(); if let Some(Value::Object(env)) = &request.env { for (key, value) in env { let value_str = match (key.as_str(), value) { (_, Value::String(value)) => value, _ => continue, }; envs.insert(key.clone(), value_str.clone()); } } let mut args = request.args.clone(); let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") { // Handle special case for NodeJS debug adapter // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection), // we set the command to None // This prevents the NodeJS REPL from appearing, which is not the desired behavior // The expected usage is for users to provide their own Node command, e.g., `node test.js` // This allows the NodeJS debug client to attach correctly if args .iter() .filter(|arg| !arg.starts_with("--")) .collect::>() .len() > 1 { Some(args.remove(0)) } else { None } } else if args.len() > 0 { Some(args.remove(0)) } else { None }; let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); let title = request .title .clone() .filter(|title| !title.is_empty()) .or_else(|| command.clone()) .unwrap_or_else(|| "Debug terminal".to_string()); let kind = TerminalKind::Task(task::SpawnInTerminal { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), command: command.clone(), args, command_label: title.clone(), cwd, env: envs, use_new_terminal: true, allow_concurrent_runs: true, reveal: task::RevealStrategy::NoFocus, reveal_target: task::RevealTarget::Dock, hide: task::HideStrategy::Never, shell, show_summary: false, show_command: false, show_rerun: false, }); let workspace = self.workspace.clone(); let weak_project = project.downgrade(); let terminal_task = project.update(cx, |project, cx| { project.create_terminal(kind, window.window_handle(), cx) }); let terminal_task = cx.spawn_in(window, async move |_, cx| { let terminal = terminal_task.await?; let terminal_view = cx.new_window_entity(|window, cx| { TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx) })?; running.update_in(cx, |running, window, cx| { running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx); running.debug_terminal.update(cx, |debug_terminal, cx| { debug_terminal.terminal = Some(terminal_view); cx.notify(); }); })?; terminal.read_with(cx, |terminal, _| { terminal .pty_info .pid() .map(|pid| pid.as_u32()) .context("Terminal was spawned but PID was not available") })? }); cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) }) } fn create_sub_view( &self, item_kind: DebuggerPaneItem, _pane: &Entity, cx: &mut Context, ) -> Box { match item_kind { DebuggerPaneItem::Console => Box::new(SubView::console(self.console.clone(), cx)), DebuggerPaneItem::Variables => Box::new(SubView::new( self.variable_list.focus_handle(cx), self.variable_list.clone().into(), item_kind, cx, )), DebuggerPaneItem::BreakpointList => { Box::new(SubView::breakpoint_list(self.breakpoint_list.clone(), cx)) } DebuggerPaneItem::Frames => Box::new(SubView::new( self.stack_frame_list.focus_handle(cx), self.stack_frame_list.clone().into(), item_kind, cx, )), DebuggerPaneItem::Modules => Box::new(SubView::new( self.module_list.focus_handle(cx), self.module_list.clone().into(), item_kind, cx, )), DebuggerPaneItem::LoadedSources => Box::new(SubView::new( self.loaded_sources_list.focus_handle(cx), self.loaded_sources_list.clone().into(), item_kind, cx, )), DebuggerPaneItem::Terminal => Box::new(SubView::new( self.debug_terminal.focus_handle(cx), self.debug_terminal.clone().into(), item_kind, cx, )), } } pub(crate) fn ensure_pane_item( &mut self, item_kind: DebuggerPaneItem, window: &mut Window, cx: &mut Context, ) { if self.pane_items_status(cx).get(&item_kind) == Some(&true) { return; }; let pane = self.panes.last_pane(); let sub_view = self.create_sub_view(item_kind, &pane, cx); pane.update(cx, |pane, cx| { pane.add_item_inner(sub_view, false, false, false, None, window, cx); }) } pub(crate) fn add_pane_item( &mut self, item_kind: DebuggerPaneItem, position: Point, window: &mut Window, cx: &mut Context, ) { debug_assert!( item_kind.is_supported(self.session.read(cx).capabilities()), "We should only allow adding supported item kinds" ); if let Some(pane) = self.panes.pane_at_pixel_position(position) { let sub_view = self.create_sub_view(item_kind, pane, cx); pane.update(cx, |pane, cx| { pane.add_item(sub_view, false, false, None, window, cx); }) } } pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap { let caps = self.session.read(cx).capabilities(); let mut pane_item_status = IndexMap::from_iter( DebuggerPaneItem::all() .iter() .filter(|kind| kind.is_supported(&caps)) .map(|kind| (*kind, false)), ); self.panes.panes().iter().for_each(|pane| { pane.read(cx) .items() .filter_map(|item| item.act_as::(cx)) .for_each(|view| { pane_item_status.insert(view.read(cx).kind, true); }); }); pane_item_status } pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context) { if self._schedule_serialize.is_none() { self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| { cx.background_executor() .timer(Duration::from_millis(100)) .await; let Some((adapter_name, pane_layout)) = this .read_with(cx, |this, cx| { let adapter_name = this.session.read(cx).adapter(); ( adapter_name, persistence::build_serialized_layout( &this.panes.root, this.dock_axis, cx, ), ) }) .ok() else { return; }; persistence::serialize_pane_layout(adapter_name, pane_layout) .await .log_err(); this.update(cx, |this, _| { this._schedule_serialize.take(); }) .ok(); })); } } pub(crate) fn handle_pane_event( this: &mut RunningState, source_pane: &Entity, event: &Event, window: &mut Window, cx: &mut Context, ) { this.serialize_layout(window, cx); match event { Event::Remove { .. } => { let _did_find_pane = this.panes.remove(&source_pane).is_ok(); debug_assert!(_did_find_pane); cx.notify(); } Event::Focus => { this.active_pane = source_pane.clone(); } _ => {} } } pub(crate) fn activate_pane_in_direction( &mut self, direction: SplitDirection, window: &mut Window, cx: &mut Context, ) { let active_pane = self.active_pane.clone(); if let Some(pane) = self .panes .find_pane_in_direction(&active_pane, direction, cx) { pane.update(cx, |pane, cx| { pane.focus_active_item(window, cx); }) } else { self.workspace .update(cx, |workspace, cx| { workspace.activate_pane_in_direction(direction, window, cx) }) .ok(); } } pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context) { if self.thread_id.is_some() { self.stack_frame_list .update(cx, |list, cx| { let Some(stack_frame_id) = list.opened_stack_frame_id() else { return Task::ready(Ok(())); }; list.go_to_stack_frame(stack_frame_id, window, cx) }) .detach(); } } pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool { self.variable_list.read(cx).has_open_context_menu() } pub fn session(&self) -> &Entity { &self.session } pub fn session_id(&self) -> SessionId { self.session_id } pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option { self.stack_frame_list.read(cx).opened_stack_frame_id() } pub(crate) fn stack_frame_list(&self) -> &Entity { &self.stack_frame_list } #[cfg(test)] pub fn console(&self) -> &Entity { &self.console } #[cfg(test)] pub(crate) fn module_list(&self) -> &Entity { &self.module_list } pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) { let (variable_list_position, pane) = self .panes .panes() .into_iter() .find_map(|pane| { pane.read(cx) .items_of_type::() .position(|view| view.read(cx).view_kind() == item) .map(|view| (view, pane)) }) .unwrap(); pane.update(cx, |this, cx| { this.activate_item(variable_list_position, true, true, window, cx); }) } #[cfg(test)] pub(crate) fn variable_list(&self) -> &Entity { &self.variable_list } #[cfg(test)] pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout { persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx) } pub fn capabilities(&self, cx: &App) -> Capabilities { self.session().read(cx).capabilities().clone() } pub fn select_current_thread( &mut self, threads: &Vec<(Thread, ThreadStatus)>, window: &mut Window, cx: &mut Context, ) { let selected_thread = self .thread_id .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0)) .or_else(|| threads.first()); let Some((selected_thread, _)) = selected_thread else { return; }; if Some(ThreadId(selected_thread.id)) != self.thread_id { self.select_thread(ThreadId(selected_thread.id), window, cx); } } pub fn selected_thread_id(&self) -> Option { self.thread_id } pub fn thread_status(&self, cx: &App) -> Option { self.thread_id .map(|id| self.session().read(cx).thread_status(id)) } pub(crate) fn select_thread( &mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context, ) { if self.thread_id.is_some_and(|id| id == thread_id) { return; } self.thread_id = Some(thread_id); self.stack_frame_list .update(cx, |list, cx| list.schedule_refresh(true, window, cx)); } pub fn continue_thread(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; self.session().update(cx, |state, cx| { state.continue_thread(thread_id, cx); }); } pub fn step_over(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { state.step_over(thread_id, granularity, cx); }); } pub(crate) fn step_in(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { state.step_in(thread_id, granularity, cx); }); } pub(crate) fn step_out(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { state.step_out(thread_id, granularity, cx); }); } pub(crate) fn step_back(&mut self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { state.step_back(thread_id, granularity, cx); }); } pub fn rerun_session(&mut self, window: &mut Window, cx: &mut Context) { if let Some((scenario, context)) = self.scenario.take().zip(self.scenario_context.take()) && scenario.build.is_some() { let DebugScenarioContext { task_context, active_buffer, worktree_id, } = context; let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade()); self.workspace .update(cx, |workspace, cx| { workspace.start_debug_session( scenario, task_context, active_buffer, worktree_id, window, cx, ) }) .ok(); } else { self.restart_session(cx); } } pub fn restart_session(&self, cx: &mut Context) { self.session().update(cx, |state, cx| { state.restart(None, cx); }); } pub fn pause_thread(&self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; self.session().update(cx, |state, cx| { state.pause_thread(thread_id, cx); }); } pub(crate) fn shutdown(&mut self, cx: &mut Context) { self.workspace .update(cx, |workspace, cx| { workspace .project() .read(cx) .breakpoint_store() .update(cx, |store, cx| { store.remove_active_position(Some(self.session_id), cx) }) }) .log_err(); self.session.update(cx, |session, cx| { session.shutdown(cx).detach(); }) } pub fn stop_thread(&self, cx: &mut Context) { let Some(thread_id) = self.thread_id else { return; }; self.workspace .update(cx, |workspace, cx| { workspace .project() .read(cx) .breakpoint_store() .update(cx, |store, cx| { store.remove_active_position(Some(self.session_id), cx) }) }) .log_err(); self.session().update(cx, |state, cx| { state.terminate_threads(Some(vec![thread_id; 1]), cx); }); } pub fn detach_client(&self, cx: &mut Context) { self.session().update(cx, |state, cx| { state.disconnect_client(cx); }); } pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { self.session.update(cx, |session, cx| { session.toggle_ignore_breakpoints(cx).detach(); }); } fn default_pane_layout( project: Entity, workspace: &WeakEntity, stack_frame_list: &Entity, variable_list: &Entity, console: &Entity, breakpoints: &Entity, debug_terminal: &Entity, dock_axis: Axis, subscriptions: &mut HashMap, window: &mut Window, cx: &mut Context<'_, RunningState>, ) -> Member { let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); leftmost_pane.update(cx, |this, cx| { this.add_item( Box::new(SubView::new( this.focus_handle(cx), stack_frame_list.clone().into(), DebuggerPaneItem::Frames, cx, )), true, false, None, window, cx, ); this.add_item( Box::new(SubView::breakpoint_list(breakpoints.clone(), cx)), true, false, None, window, cx, ); this.activate_item(0, false, false, window, cx); }); let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); center_pane.update(cx, |this, cx| { let view = SubView::console(console.clone(), cx); this.add_item(Box::new(view), true, false, None, window, cx); this.add_item( Box::new(SubView::new( variable_list.focus_handle(cx), variable_list.clone().into(), DebuggerPaneItem::Variables, cx, )), true, false, None, window, cx, ); this.activate_item(0, false, false, window, cx); }); let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); rightmost_pane.update(cx, |this, cx| { this.add_item( Box::new(SubView::new( debug_terminal.focus_handle(cx), debug_terminal.clone().into(), DebuggerPaneItem::Terminal, cx, )), false, false, None, window, cx, ); }); subscriptions.extend( [&leftmost_pane, ¢er_pane, &rightmost_pane] .into_iter() .map(|entity| { ( entity.entity_id(), cx.subscribe_in(entity, window, Self::handle_pane_event), ) }), ); let group_root = workspace::PaneAxis::new( dock_axis.invert(), [leftmost_pane, center_pane, rightmost_pane] .into_iter() .map(workspace::Member::Pane) .collect(), ); Member::Axis(group_root) } pub(crate) fn invert_axies(&mut self) { self.dock_axis = self.dock_axis.invert(); self.panes.invert_axies(); } } impl EventEmitter for RunningState {} impl Focusable for RunningState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } }