debugger: Add breakpoint list (#28496)

Piotr Osiewicz and Anthony Eid created

![image](https://github.com/user-attachments/assets/2cbe60cc-bf04-4233-a7bc-32affff8eef5)
Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

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 
crates/debugger_ui/src/session/running/breakpoint_list.rs | 482 +++++++++
crates/debugger_ui/src/session/running/console.rs         |   5 
crates/icons/src/icons.rs                                 |   3 
crates/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(-)

Detailed changes

assets/icons/binary.svg 🔗

@@ -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-binary-icon lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>

assets/icons/flame.svg 🔗

@@ -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-flame-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>

assets/icons/function.svg 🔗

@@ -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-square-function-icon lucide-square-function"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"/><path d="M9 11.2h5.7"/></svg>

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());

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();
                                                     }
                                                 }),
                                             )

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| {

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<Workspace>,
+    breakpoint_store: Entity<BreakpointStore>,
+    worktree_store: Entity<WorktreeStore>,
+    list_state: ListState,
+    scrollbar_state: ScrollbarState,
+    breakpoints: Vec<BreakpointEntry>,
+    session: Entity<Session>,
+    hide_scrollbar_task: Option<Task<()>>,
+    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<Session>,
+        workspace: WeakEntity<Workspace>,
+        project: &Entity<Project>,
+        cx: &mut App,
+    ) -> Entity<Self> {
+        let project = project.read(cx);
+        let breakpoint_store = project.breakpoint_store();
+        let worktree_store = project.worktree_store();
+
+        cx.new(|cx| {
+            let weak: gpui::WeakEntity<Self> = 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<Self>) {
+        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<Self>) -> Option<Stateful<Div>> {
+        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<Self>,
+    ) -> 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<SharedString>,
+    line: SharedString,
+    breakpoint: SourceBreakpoint,
+}
+
+impl LineBreakpoint {
+    fn render(self, weak: WeakEntity<BreakpointList>) -> 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>() {
+                                    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<BreakpointList>) -> 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<BreakpointList>,
+}
+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)
+            }
+        }
+    }
+}

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<Editor>,
@@ -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()

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,

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<Buffer>, (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::<Point>(&snapshot).row == row)
+                .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
+        })
+    }
+
     pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
         self.breakpoints
             .get(path)

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<String>,
+    },
+    WithOptions {
+        filters: Vec<ExceptionFilterOptions>,
+    },
+}
+
+impl LocalDapCommand for SetExceptionBreakpoints {
+    type Response = Vec<dap::Breakpoint>;
+    type DapRequest = dap::requests::SetExceptionBreakpoints;
+
+    fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::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: <Self::DapRequest as dap::requests::Request>::Response,
+    ) -> Result<Self::Response> {
+        Ok(message.breakpoints.unwrap_or_default())
+    }
+}
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub(super) struct LocationsCommand {

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| {

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::<dap::requests::SetExceptionBreakpoints, _>(move |_, _| {
+                    Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None })
+                })
+                .await;
+
             session
                 .client
                 .on_request::<dap::requests::Disconnect, _>(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<ExceptionBreakpointsFilter>,
+        supports_filter_options: bool,
+        cx: &App,
+    ) -> Task<Result<Vec<dap::Breakpoint>>> {
+        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::<Vec<_>>()
+            })
+            .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<u64, dap::LocationsResponse>,
     is_session_terminated: bool,
     requests: HashMap<TypeId, HashMap<RequestSlot, Shared<Task<Option<()>>>>>,
+    exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
     _background_tasks: Vec<Task<()>>,
 }
 
@@ -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<Item = &(ExceptionBreakpointsFilter, IsEnabled)> {
+        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,
     }