Detailed changes
@@ -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>
@@ -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>
@@ -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>
@@ -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());
@@ -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();
}
}),
)
@@ -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| {
@@ -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)
+ }
+ }
+ }
+}
@@ -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()
@@ -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,
@@ -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)
@@ -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 {
@@ -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| {
@@ -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,
}