From 26f4705198aea25bc0c89bbe2ba0cc00f2c63c50 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:18:58 +0200 Subject: [PATCH] debugger: Add breakpoint list (#28496) ![image](https://github.com/user-attachments/assets/2cbe60cc-bf04-4233-a7bc-32affff8eef5) Release Notes: - N/A --------- Co-authored-by: Anthony Eid --- assets/icons/binary.svg | 1 + assets/icons/flame.svg | 1 + assets/icons/function.svg | 1 + crates/dap_adapters/src/codelldb.rs | 8 + crates/debugger_ui/src/debugger_panel.rs | 21 +- crates/debugger_ui/src/session/running.rs | 17 + .../src/session/running/breakpoint_list.rs | 482 ++++++++++++++++++ .../src/session/running/console.rs | 5 +- crates/icons/src/icons.rs | 3 + .../project/src/debugger/breakpoint_store.rs | 19 +- crates/project/src/debugger/dap_command.rs | 40 +- crates/project/src/debugger/dap_store.rs | 6 +- crates/project/src/debugger/session.rs | 125 ++++- 13 files changed, 711 insertions(+), 18 deletions(-) create mode 100644 assets/icons/binary.svg create mode 100644 assets/icons/flame.svg create mode 100644 assets/icons/function.svg create mode 100644 crates/debugger_ui/src/session/running/breakpoint_list.rs diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg new file mode 100644 index 0000000000000000000000000000000000000000..8f5e456d165dda9f39491871732224339d8f2c9f --- /dev/null +++ b/assets/icons/binary.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg new file mode 100644 index 0000000000000000000000000000000000000000..075e027a5c9634473d996c588234501da77fd26b --- /dev/null +++ b/assets/icons/flame.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/function.svg b/assets/icons/function.svg new file mode 100644 index 0000000000000000000000000000000000000000..5d0b9d58ef7dbb2c570151c3e08305d38e875ec2 --- /dev/null +++ b/assets/icons/function.svg @@ -0,0 +1 @@ + diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 2d076484273b8b8fa5be442fcb62b0efe2600bf0..08dc03707aa4573d5cddca75a5c8a41927dac6c2 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -105,6 +105,12 @@ impl DebugAdapter for CodeLldbDebugAdapter { Ok(DebugAdapterBinary { command, cwd: Some(adapter_dir), + arguments: Some(vec![ + "--settings".into(), + json!({"sourceLanguages": ["cpp", "rust"]}) + .to_string() + .into(), + ]), ..Default::default() }) } @@ -117,6 +123,8 @@ impl DebugAdapter for CodeLldbDebugAdapter { }, }); let map = args.as_object_mut().unwrap(); + // CodeLLDB uses `name` for a terminal label. + map.insert("name".into(), Value::String(config.label.clone())); match &config.request { DebugRequestType::Attach(attach) => { map.insert("pid".into(), attach.process_id.into()); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 63496e41e6ae9fd7396405df2ca289db8c119b2d..0586ff24aca6e5c22e3da4f8983331f392ce71a8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -417,7 +417,8 @@ impl DebugPanel { DropdownMenu::new_with_element( "debugger-session-list", label, - ContextMenu::build(window, cx, move |mut this, _, _| { + ContextMenu::build(window, cx, move |mut this, _, cx| { + let context_menu = cx.weak_entity(); for session in sessions.into_iter() { let weak_session = session.downgrade(); let weak_session_id = weak_session.entity_id(); @@ -425,11 +426,17 @@ impl DebugPanel { this = this.custom_entry( { let weak = weak.clone(); + let context_menu = context_menu.clone(); move |_, cx| { weak_session .read_with(cx, |session, cx| { + let context_menu = context_menu.clone(); + let id: SharedString = + format!("debug-session-{}", session.session_id(cx).0) + .into(); h_flex() .w_full() + .group(id.clone()) .justify_between() .child(session.label_element(cx)) .child( @@ -437,15 +444,25 @@ impl DebugPanel { "close-debug-session", IconName::Close, ) + .visible_on_hover(id.clone()) .icon_size(IconSize::Small) .on_click({ let weak = weak.clone(); - move |_, _, cx| { + move |_, window, cx| { weak.update(cx, |panel, cx| { panel .close_session(weak_session_id, cx); }) .ok(); + context_menu + .update(cx, |this, cx| { + this.cancel( + &Default::default(), + window, + cx, + ); + }) + .ok(); } }), ) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 35009b08a20cdb3ad729b918bdd7e59d261daea3..a994634cf7dbfdb6806903ab7d426228b66e24ea 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1,3 +1,4 @@ +mod breakpoint_list; mod console; mod loaded_source_list; mod module_list; @@ -7,6 +8,7 @@ pub mod variable_list; use std::{any::Any, ops::ControlFlow, sync::Arc}; use super::DebugPanelItemEvent; +use breakpoint_list::BreakpointList; use collections::HashMap; use console::Console; use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings}; @@ -321,6 +323,21 @@ impl RunningState { window, cx, ); + let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx); + this.add_item( + Box::new(SubView::new( + breakpoints.focus_handle(cx), + breakpoints.into(), + SharedString::new_static("Breakpoints"), + 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| { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..3a9ddd1869acf6a97aa699e1a7ba1c6db555e4a2 --- /dev/null +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -0,0 +1,482 @@ +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; + +use dap::ExceptionBreakpointsFilter; +use editor::Editor; +use gpui::{ + AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity, + list, +}; +use language::Point; +use project::{ + Project, + debugger::{ + breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint}, + session::Session, + }, + worktree_store::WorktreeStore, +}; +use ui::{ + App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement, + IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce, + Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div, + h_flex, px, v_flex, +}; +use util::{ResultExt, maybe}; +use workspace::Workspace; + +pub(super) struct BreakpointList { + workspace: WeakEntity, + breakpoint_store: Entity, + worktree_store: Entity, + list_state: ListState, + scrollbar_state: ScrollbarState, + breakpoints: Vec, + session: Entity, + hide_scrollbar_task: Option>, + show_scrollbar: bool, + focus_handle: FocusHandle, +} + +impl Focusable for BreakpointList { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} +impl BreakpointList { + pub(super) fn new( + session: Entity, + workspace: WeakEntity, + project: &Entity, + cx: &mut App, + ) -> Entity { + let project = project.read(cx); + let breakpoint_store = project.breakpoint_store(); + let worktree_store = project.worktree_store(); + + cx.new(|cx| { + let weak: gpui::WeakEntity = cx.weak_entity(); + let list_state = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, window, cx| { + let Ok(Some(breakpoint)) = + weak.update(cx, |this, _| this.breakpoints.get(ix).cloned()) + else { + return div().into_any_element(); + }; + + breakpoint.render(window, cx).into_any_element() + }, + ); + Self { + breakpoint_store, + worktree_store, + scrollbar_state: ScrollbarState::new(list_state.clone()), + list_state, + breakpoints: Default::default(), + hide_scrollbar_task: None, + show_scrollbar: false, + workspace, + session, + focus_handle: cx.focus_handle(), + } + }) + } + + fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { + const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); + self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + panel + .update(cx, |panel, cx| { + panel.show_scrollbar = false; + cx.notify(); + }) + .log_err(); + })) + } + + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { + if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) { + return None; + } + Some( + div() + .occlude() + .id("breakpoint-list-vertical-scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())), + ) + } +} +impl Render for BreakpointList { + fn render( + &mut self, + _window: &mut ui::Window, + cx: &mut ui::Context, + ) -> impl ui::IntoElement { + let old_len = self.breakpoints.len(); + let breakpoints = self.breakpoint_store.read(cx).all_breakpoints(cx); + self.breakpoints.clear(); + let weak = cx.weak_entity(); + let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| { + let relative_worktree_path = self + .worktree_store + .read(cx) + .find_worktree(&path, cx) + .and_then(|(worktree, relative_path)| { + worktree + .read(cx) + .is_visible() + .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path)) + }); + breakpoints.sort_by_key(|breakpoint| breakpoint.row); + let weak = weak.clone(); + breakpoints.into_iter().filter_map(move |breakpoint| { + debug_assert_eq!(&path, &breakpoint.path); + let file_name = breakpoint.path.file_name()?; + + let dir = relative_worktree_path + .clone() + .unwrap_or_else(|| PathBuf::from(&*breakpoint.path)) + .parent() + .and_then(|parent| { + parent + .to_str() + .map(ToOwned::to_owned) + .map(SharedString::from) + }); + let name = file_name + .to_str() + .map(ToOwned::to_owned) + .map(SharedString::from)?; + let weak = weak.clone(); + let line = format!("Line {}", breakpoint.row + 1).into(); + Some(BreakpointEntry { + kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint { + name, + dir, + line, + breakpoint, + }), + weak, + }) + }) + }); + let exception_breakpoints = + self.session + .read(cx) + .exception_breakpoints() + .map(|(data, is_enabled)| BreakpointEntry { + kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint { + id: data.filter.clone(), + data: data.clone(), + is_enabled: *is_enabled, + }), + weak: weak.clone(), + }); + self.breakpoints + .extend(breakpoints.chain(exception_breakpoints)); + if self.breakpoints.len() != old_len { + self.list_state.reset(self.breakpoints.len()); + } + v_flex() + .id("breakpoint-list") + .on_hover(cx.listener(|this, hovered, window, cx| { + if *hovered { + this.show_scrollbar = true; + this.hide_scrollbar_task.take(); + cx.notify(); + } else if !this.focus_handle.contains_focused(window, cx) { + this.hide_scrollbar(window, cx); + } + })) + .size_full() + .m_0p5() + .child(list(self.list_state.clone()).flex_grow()) + .children(self.render_vertical_scrollbar(cx)) + } +} +#[derive(Clone, Debug)] +struct LineBreakpoint { + name: SharedString, + dir: Option, + line: SharedString, + breakpoint: SourceBreakpoint, +} + +impl LineBreakpoint { + fn render(self, weak: WeakEntity) -> ListItem { + let LineBreakpoint { + name, + dir, + line, + breakpoint, + } = self; + let icon_name = if breakpoint.state.is_enabled() { + IconName::DebugBreakpoint + } else { + IconName::DebugDisabledBreakpoint + }; + let path = breakpoint.path; + let row = breakpoint.row; + let indicator = div() + .id(SharedString::from(format!( + "breakpoint-ui-toggle-{:?}/{}:{}", + dir, name, line + ))) + .cursor_pointer() + .on_click({ + let weak = weak.clone(); + let path = path.clone(); + move |_, _, cx| { + weak.update(cx, |this, cx| { + this.breakpoint_store.update(cx, |this, cx| { + if let Some((buffer, breakpoint)) = + this.breakpoint_at_row(&path, row, cx) + { + this.toggle_breakpoint( + buffer, + breakpoint, + BreakpointEditAction::InvertState, + cx, + ); + } else { + log::error!("Couldn't find breakpoint at row event though it exists: row {row}") + } + }) + }) + .ok(); + } + }) + .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger)) + .on_mouse_down(MouseButton::Left, move |_, _, _| {}); + ListItem::new(SharedString::from(format!( + "breakpoint-ui-item-{:?}/{}:{}", + dir, name, line + ))) + .start_slot(indicator) + .rounded() + .end_hover_slot( + IconButton::new( + SharedString::from(format!( + "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", + dir, name, line + )), + IconName::Close, + ) + .on_click({ + let weak = weak.clone(); + let path = path.clone(); + move |_, _, cx| { + weak.update(cx, |this, cx| { + this.breakpoint_store.update(cx, |this, cx| { + if let Some((buffer, breakpoint)) = + this.breakpoint_at_row(&path, row, cx) + { + this.toggle_breakpoint( + buffer, + breakpoint, + BreakpointEditAction::Toggle, + cx, + ); + } else { + log::error!("Couldn't find breakpoint at row event though it exists: row {row}") + } + }) + }) + .ok(); + } + }) + .icon_size(ui::IconSize::XSmall), + ) + .child( + v_flex() + .id(SharedString::from(format!( + "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", + dir, name, line + ))) + .on_click(move |_, window, cx| { + let path = path.clone(); + let weak = weak.clone(); + let row = breakpoint.row; + maybe!({ + let task = weak + .update(cx, |this, cx| { + this.worktree_store.update(cx, |this, cx| { + this.find_or_create_worktree(path, false, cx) + }) + }) + .ok()?; + window + .spawn(cx, async move |cx| { + let (worktree, relative_path) = task.await?; + let worktree_id = worktree.update(cx, |this, _| this.id())?; + let item = weak + .update_in(cx, |this, window, cx| { + this.workspace.update(cx, |this, cx| { + this.open_path( + (worktree_id, relative_path), + None, + true, + window, + cx, + ) + }) + })?? + .await?; + if let Some(editor) = item.downcast::() { + editor + .update_in(cx, |this, window, cx| { + this.go_to_singleton_buffer_point( + Point { row, column: 0 }, + window, + cx, + ); + }) + .ok(); + } + Result::<_, anyhow::Error>::Ok(()) + }) + .detach(); + + Some(()) + }); + }) + .cursor_pointer() + .py_1() + .items_center() + .child( + h_flex() + .gap_1() + .child( + Label::new(name) + .size(LabelSize::Small) + .line_height_style(ui::LineHeightStyle::UiLabel), + ) + .children(dir.map(|dir| { + Label::new(dir) + .color(Color::Muted) + .size(LabelSize::Small) + .line_height_style(ui::LineHeightStyle::UiLabel) + })), + ) + .child( + Label::new(line) + .size(LabelSize::XSmall) + .color(Color::Muted) + .line_height_style(ui::LineHeightStyle::UiLabel), + ), + ) + } +} +#[derive(Clone, Debug)] +struct ExceptionBreakpoint { + id: String, + data: ExceptionBreakpointsFilter, + is_enabled: bool, +} + +impl ExceptionBreakpoint { + fn render(self, list: WeakEntity) -> ListItem { + let color = if self.is_enabled { + Color::Debugger + } else { + Color::Muted + }; + let id = SharedString::from(&self.id); + ListItem::new(SharedString::from(format!( + "exception-breakpoint-ui-item-{}", + self.id + ))) + .rounded() + .start_slot( + div() + .id(SharedString::from(format!( + "exception-breakpoint-ui-item-{}-click-handler", + self.id + ))) + .on_click(move |_, _, cx| { + list.update(cx, |this, cx| { + this.session.update(cx, |this, cx| { + this.toggle_exception_breakpoint(&id, cx); + }); + cx.notify(); + }) + .ok(); + }) + .cursor_pointer() + .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)), + ) + .child( + div() + .py_1() + .gap_1() + .child( + Label::new(self.data.label) + .size(LabelSize::Small) + .line_height_style(ui::LineHeightStyle::UiLabel), + ) + .children(self.data.description.map(|description| { + Label::new(description) + .size(LabelSize::XSmall) + .line_height_style(ui::LineHeightStyle::UiLabel) + .color(Color::Muted) + })), + ) + } +} +#[derive(Clone, Debug)] +enum BreakpointEntryKind { + LineBreakpoint(LineBreakpoint), + ExceptionBreakpoint(ExceptionBreakpoint), +} + +#[derive(Clone, Debug)] +struct BreakpointEntry { + kind: BreakpointEntryKind, + weak: WeakEntity, +} +impl RenderOnce for BreakpointEntry { + fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement { + match self.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + line_breakpoint.render(self.weak) + } + BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { + exception_breakpoint.render(self.weak) + } + } + } +} diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index e8fad5b4437069dea71202d97554b9b6004f3491..ece97efde3365080373ddaadd8c11d0cdd5679f1 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -17,7 +17,7 @@ use project::{ use settings::Settings; use std::{cell::RefCell, rc::Rc, usize}; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{Divider, prelude::*}; pub struct Console { console: Entity, @@ -229,7 +229,8 @@ impl Render for Console { .size_full() .child(self.render_console(cx)) .when(self.is_local(cx), |this| { - this.child(self.render_query_bar(cx)) + this.child(Divider::horizontal()) + .child(self.render_query_bar(cx)) .pt(DynamicSpacing::Base04.rems(cx)) }) .border_2() diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 7e419fa59aa6da2a99f3116713f547c1d41dafa4..aa8dcaf587a1e1809a44d79ac3d611b9955e8a2b 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -39,6 +39,7 @@ pub enum IconName { BellDot, BellOff, BellRing, + Binary, Blocks, Bolt, Book, @@ -119,6 +120,7 @@ pub enum IconName { FileToml, FileTree, Filter, + Flame, Folder, FolderOpen, FolderX, @@ -126,6 +128,7 @@ pub enum IconName { FontSize, FontWeight, ForwardArrow, + Function, GenericClose, GenericMaximize, GenericMinimize, diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index a5e5a134b17b7d4474bb00b7032c9d0bad962047..31ef65dab3fb2c70d6e48f9ce7d57dad73e63113 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -12,7 +12,7 @@ use rpc::{ proto::{self}, }; use std::{hash::Hash, ops::Range, path::Path, sync::Arc}; -use text::PointUtf16; +use text::{Point, PointUtf16}; use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; @@ -464,6 +464,23 @@ impl BreakpointStore { cx.notify(); } + pub fn breakpoint_at_row( + &self, + path: &Path, + row: u32, + cx: &App, + ) -> Option<(Entity, (text::Anchor, Breakpoint))> { + self.breakpoints.get(path).and_then(|breakpoints| { + let snapshot = breakpoints.buffer.read(cx).text_snapshot(); + + breakpoints + .breakpoints + .iter() + .find(|(anchor, _)| anchor.summary::(&snapshot).row == row) + .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone())) + }) + } + pub fn breakpoints_from_path(&self, path: &Arc, cx: &App) -> Vec { self.breakpoints .get(path) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 60fade53ebe1cf43957146ddb89576ad7a0c85a5..4eac9f0c285f375df2820de219b1bf73993e84e2 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{Ok, Result, anyhow}; use dap::{ - Capabilities, ContinueArguments, InitializeRequestArguments, + Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint, StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, @@ -1665,6 +1665,44 @@ impl LocalDapCommand for SetBreakpoints { Ok(message.breakpoints) } } +#[derive(Clone, Debug, Hash, PartialEq)] +pub(super) enum SetExceptionBreakpoints { + Plain { + filters: Vec, + }, + WithOptions { + filters: Vec, + }, +} + +impl LocalDapCommand for SetExceptionBreakpoints { + type Response = Vec; + type DapRequest = dap::requests::SetExceptionBreakpoints; + + fn to_dap(&self) -> ::Arguments { + match self { + SetExceptionBreakpoints::Plain { filters } => dap::SetExceptionBreakpointsArguments { + filters: filters.clone(), + exception_options: None, + filter_options: None, + }, + SetExceptionBreakpoints::WithOptions { filters } => { + dap::SetExceptionBreakpointsArguments { + filters: vec![], + filter_options: Some(filters.clone()), + exception_options: None, + } + } + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.breakpoints.unwrap_or_default()) + } +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub(super) struct LocationsCommand { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 23f72440a5fed0118e5e4c1052b428e290bf4983..fcc557f80903f79700b2323b957ea6f43fe5a01e 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -852,8 +852,7 @@ fn create_new_session( cx.emit(DapStoreEvent::DebugClientStarted(session_id)); cx.notify(); })?; - - match { + let seq_result = { session .update(cx, |session, cx| session.request_initialize(cx))? .await?; @@ -863,7 +862,8 @@ fn create_new_session( session.initialize_sequence(initialized_rx, cx) })? .await - } { + }; + match seq_result { Ok(_) => {} Err(error) => { this.update(cx, |this, cx| { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 47b082fe918970f6959a8aad96dce8a2a95da36d..54f3550d2e87b393bbd7a43f6b9940c390ec7f5c 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -7,9 +7,9 @@ use super::dap_command::{ self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, - ScopesCommand, SetVariableValueCommand, StackTraceCommand, StepBackCommand, StepCommand, - StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, ThreadsCommand, - VariablesCommand, + ScopesCommand, SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, + StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, + TerminateThreadsCommand, ThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{Context as _, Result, anyhow}; @@ -23,7 +23,10 @@ use dap::{ client::{DebugAdapterClient, SessionId}, messages::{Events, Message}, }; -use dap::{DapRegistry, DebugRequestType, OutputEventCategory}; +use dap::{ + DapRegistry, DebugRequestType, ExceptionBreakpointsFilter, ExceptionFilterOptions, + OutputEventCategory, +}; use futures::channel::oneshot; use futures::{FutureExt, future::Shared}; use gpui::{ @@ -34,6 +37,7 @@ use serde_json::{Value, json}; use settings::Settings; use smol::stream::StreamExt; use std::any::TypeId; +use std::collections::BTreeMap; use std::path::PathBuf; use std::u64; use std::{ @@ -324,6 +328,13 @@ impl LocalMode { } } + session + .client + .on_request::(move |_, _| { + Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None }) + }) + .await; + session .client .on_request::(move |_, _| Ok(())) @@ -456,7 +467,31 @@ impl LocalMode { }) } - fn send_all_breakpoints(&self, ignore_breakpoints: bool, cx: &App) -> Task<()> { + fn send_exception_breakpoints( + &self, + filters: Vec, + supports_filter_options: bool, + cx: &App, + ) -> Task>> { + let arg = if supports_filter_options { + SetExceptionBreakpoints::WithOptions { + filters: filters + .into_iter() + .map(|filter| ExceptionFilterOptions { + filter_id: filter.filter, + condition: None, + mode: None, + }) + .collect(), + } + } else { + SetExceptionBreakpoints::Plain { + filters: filters.into_iter().map(|filter| filter.filter).collect(), + } + }; + self.request(arg, cx.background_executor().clone()) + } + fn send_source_breakpoints(&self, ignore_breakpoints: bool, cx: &App) -> Task<()> { let mut breakpoint_tasks = Vec::new(); let breakpoints = self .breakpoint_store @@ -588,15 +623,37 @@ impl LocalMode { }; let configuration_done_supported = ConfigurationDone::is_supported(capabilities); - + let exception_filters = capabilities + .exception_breakpoint_filters + .as_ref() + .map(|exception_filters| { + exception_filters + .iter() + .filter(|filter| filter.default == Some(true)) + .cloned() + .collect::>() + }) + .unwrap_or_default(); + let supports_exception_filters = capabilities + .supports_exception_filter_options + .unwrap_or_default(); let configuration_sequence = cx.spawn({ let this = self.clone(); async move |cx| { initialized_rx.await?; // todo(debugger) figure out if we want to handle a breakpoint response error // This will probably consist of letting a user know that breakpoints failed to be set - cx.update(|cx| this.send_all_breakpoints(false, cx))?.await; - + cx.update(|cx| this.send_source_breakpoints(false, cx))? + .await; + cx.update(|cx| { + this.send_exception_breakpoints( + exception_filters, + supports_exception_filters, + cx, + ) + })? + .await + .ok(); if configuration_done_supported { this.request(ConfigurationDone {}, cx.background_executor().clone()) } else { @@ -727,6 +784,8 @@ impl ThreadStates { } const MAX_TRACKED_OUTPUT_EVENTS: usize = 5000; +type IsEnabled = bool; + #[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord)] pub struct OutputToken(pub usize); /// Represents a current state of a single debug adapter and provides ways to mutate it. @@ -748,6 +807,7 @@ pub struct Session { locations: HashMap, is_session_terminated: bool, requests: HashMap>>>>, + exception_breakpoints: BTreeMap, _background_tasks: Vec>, } @@ -956,6 +1016,7 @@ impl Session { _background_tasks: Vec::default(), locations: Default::default(), is_session_terminated: false, + exception_breakpoints: Default::default(), } } @@ -1022,6 +1083,18 @@ impl Session { let capabilities = capabilities.await?; this.update(cx, |session, _| { session.capabilities = capabilities; + let filters = session + .capabilities + .exception_breakpoint_filters + .clone() + .unwrap_or_default(); + for filter in filters { + let default = filter.default.unwrap_or_default(); + session + .exception_breakpoints + .entry(filter.filter.clone()) + .or_insert_with(|| (filter, default)); + } })?; Ok(()) }) @@ -1464,13 +1537,46 @@ impl Session { self.ignore_breakpoints = ignore; if let Some(local) = self.as_local() { - local.send_all_breakpoints(ignore, cx) + local.send_source_breakpoints(ignore, cx) } else { // todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions unimplemented!() } } + pub fn exception_breakpoints( + &self, + ) -> impl Iterator { + self.exception_breakpoints.values() + } + + pub fn toggle_exception_breakpoint(&mut self, id: &str, cx: &App) { + if let Some((_, is_enabled)) = self.exception_breakpoints.get_mut(id) { + *is_enabled = !*is_enabled; + self.send_exception_breakpoints(cx); + } + } + + fn send_exception_breakpoints(&mut self, cx: &App) { + if let Some(local) = self.as_local() { + let exception_filters = self + .exception_breakpoints + .values() + .filter_map(|(filter, is_enabled)| is_enabled.then(|| filter.clone())) + .collect(); + + let supports_exception_filters = self + .capabilities + .supports_exception_filter_options + .unwrap_or_default(); + local + .send_exception_breakpoints(exception_filters, supports_exception_filters, cx) + .detach_and_log_err(cx); + } else { + debug_assert!(false, "Not implemented"); + } + } + pub fn breakpoints_enabled(&self) -> bool { self.ignore_breakpoints } @@ -2084,6 +2190,7 @@ fn create_local_session( threads: IndexMap::default(), stack_frames: IndexMap::default(), locations: Default::default(), + exception_breakpoints: Default::default(), _background_tasks, is_session_terminated: false, }