use std::{ ops::Range, path::{Path, PathBuf}, sync::Arc, time::Duration, }; use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName}; use db::kvp::KeyValueStore; use editor::Editor; use gpui::{ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, }; use itertools::Itertools; use language::Point; use project::{ Project, debugger::{ breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint}, dap_store::{DapStore, PersistedAdapterOptions}, session::Session, }, worktree_store::WorktreeStore, }; use ui::{ Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, ScrollAxes, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*, }; use util::rel_path::RelPath; use workspace::Workspace; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; actions!( debugger, [ /// Navigates to the previous breakpoint property in the list. PreviousBreakpointProperty, /// Navigates to the next breakpoint property in the list. NextBreakpointProperty ] ); #[derive(Clone, Copy, PartialEq)] pub(crate) enum SelectedBreakpointKind { Source, Exception, Data, } pub(crate) struct BreakpointList { workspace: WeakEntity, breakpoint_store: Entity, dap_store: Entity, worktree_store: Entity, breakpoints: Vec, session: Option>, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, selected_ix: Option, max_width_index: Option, input: Entity, strip_mode: Option, serialize_exception_breakpoints_task: Option>>, } impl Focusable for BreakpointList { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } #[derive(Clone, Copy, PartialEq)] enum ActiveBreakpointStripMode { Log, Condition, HitCondition, } impl BreakpointList { pub(crate) fn new( session: Option>, workspace: WeakEntity, project: &Entity, window: &mut Window, cx: &mut App, ) -> Entity { let project = project.read(cx); let breakpoint_store = project.breakpoint_store(); let worktree_store = project.worktree_store(); let dap_store = project.dap_store(); let focus_handle = cx.focus_handle(); let scroll_handle = UniformListScrollHandle::new(); let adapter_name = session.as_ref().map(|session| session.read(cx).adapter()); cx.new(|cx| { let this = Self { breakpoint_store, dap_store, worktree_store, breakpoints: Default::default(), max_width_index: None, workspace, session, focus_handle, scroll_handle, selected_ix: None, input: cx.new(|cx| Editor::single_line(window, cx)), strip_mode: None, serialize_exception_breakpoints_task: None, }; if let Some(name) = adapter_name { _ = this.deserialize_exception_breakpoints(name, cx); } this }) } fn edit_line_breakpoint( &self, path: Arc, row: u32, action: BreakpointEditAction, cx: &mut App, ) { Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx); } fn edit_line_breakpoint_inner( breakpoint_store: &Entity, path: Arc, row: u32, action: BreakpointEditAction, cx: &mut App, ) { breakpoint_store.update(cx, |breakpoint_store, cx| { if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) { breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx); } else { log::error!("Couldn't find breakpoint at row event though it exists: row {row}") } }) } fn go_to_line_breakpoint( &mut self, path: Arc, row: u32, window: &mut Window, cx: &mut Context, ) { let task = self .worktree_store .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx)); cx.spawn_in(window, async move |this, cx| { let (worktree, relative_path) = task.await?; let worktree_id = worktree.read_with(cx, |this, _| this.id()); let item = this .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(); } anyhow::Ok(()) }) .detach(); } pub(crate) fn selection_kind(&self) -> Option<(SelectedBreakpointKind, bool)> { self.selected_ix.and_then(|ix| { self.breakpoints.get(ix).map(|bp| match &bp.kind { BreakpointEntryKind::LineBreakpoint(bp) => ( SelectedBreakpointKind::Source, bp.breakpoint.state == project::debugger::breakpoint_store::BreakpointState::Enabled, ), BreakpointEntryKind::ExceptionBreakpoint(bp) => { (SelectedBreakpointKind::Exception, bp.is_enabled) } BreakpointEntryKind::DataBreakpoint(bp) => { (SelectedBreakpointKind::Data, bp.0.is_enabled) } }) }) } fn set_active_breakpoint_property( &mut self, prop: ActiveBreakpointStripMode, window: &mut Window, cx: &mut App, ) { self.strip_mode = Some(prop); let placeholder = match prop { ActiveBreakpointStripMode::Log => "Set Log Message", ActiveBreakpointStripMode::Condition => "Set Condition", ActiveBreakpointStripMode::HitCondition => "Set Hit Condition", }; let mut is_exception_breakpoint = true; let active_value = self.selected_ix.and_then(|ix| { self.breakpoints.get(ix).and_then(|bp| { if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind { is_exception_breakpoint = false; match prop { ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(), ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(), ActiveBreakpointStripMode::HitCondition => { bp.breakpoint.hit_condition.clone() } } } else { None } }) }); self.input.update(cx, |this, cx| { this.set_placeholder_text(placeholder, window, cx); this.set_read_only(is_exception_breakpoint); this.set_text(active_value.as_deref().unwrap_or(""), window, cx); }); } fn select_ix(&mut self, ix: Option, window: &mut Window, cx: &mut Context) { self.selected_ix = ix; if let Some(ix) = ix { self.scroll_handle .scroll_to_item(ix, ScrollStrategy::Center); } if let Some(mode) = self.strip_mode { self.set_active_breakpoint_property(mode, window, cx); } cx.notify(); } fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context) { if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { cx.propagate(); return; } let ix = match self.selected_ix { _ if self.breakpoints.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.breakpoints.len() - 1 { Some(0) } else { Some(ix + 1) } } }; self.select_ix(ix, window, cx); } fn select_previous( &mut self, _: &menu::SelectPrevious, window: &mut Window, cx: &mut Context, ) { if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { cx.propagate(); return; } let ix = match self.selected_ix { _ if self.breakpoints.is_empty() => None, None => Some(self.breakpoints.len() - 1), Some(ix) => { if ix == 0 { Some(self.breakpoints.len() - 1) } else { Some(ix - 1) } } }; self.select_ix(ix, window, cx); } fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) { if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { cx.propagate(); return; } let ix = if !self.breakpoints.is_empty() { Some(0) } else { None }; self.select_ix(ix, window, cx); } fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { cx.propagate(); return; } let ix = if !self.breakpoints.is_empty() { Some(self.breakpoints.len() - 1) } else { None }; self.select_ix(ix, window, cx); } fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { if self.input.focus_handle(cx).contains_focused(window, cx) { self.focus_handle.focus(window, cx); } else if self.strip_mode.is_some() { self.strip_mode.take(); cx.notify(); } else { cx.propagate(); } } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; if let Some(mode) = self.strip_mode { let handle = self.input.focus_handle(cx); if handle.is_focused(window) { // Go back to the main strip. Save the result as well. let text = self.input.read(cx).text(cx); match mode { ActiveBreakpointStripMode::Log => { if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), line_breakpoint.breakpoint.row, BreakpointEditAction::EditLogMessage(Arc::from(text)), cx, ); } } ActiveBreakpointStripMode::Condition => { if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), line_breakpoint.breakpoint.row, BreakpointEditAction::EditCondition(Arc::from(text)), cx, ); } } ActiveBreakpointStripMode::HitCondition => { if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), line_breakpoint.breakpoint.row, BreakpointEditAction::EditHitCondition(Arc::from(text)), cx, ); } } } self.focus_handle.focus(window, cx); } else { handle.focus(window, cx); } return; } match &mut entry.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { let path = line_breakpoint.breakpoint.path.clone(); let row = line_breakpoint.breakpoint.row; self.go_to_line_breakpoint(path, row, window, cx); } BreakpointEntryKind::DataBreakpoint(_) | BreakpointEntryKind::ExceptionBreakpoint(_) => {} } } fn toggle_enable_breakpoint( &mut self, _: &ToggleEnableBreakpoint, window: &mut Window, cx: &mut Context, ) { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { cx.propagate(); return; } match &mut entry.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { let path = line_breakpoint.breakpoint.path.clone(); let row = line_breakpoint.breakpoint.row; self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx); } BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { let id = exception_breakpoint.id.clone(); self.toggle_exception_breakpoint(&id, cx); } BreakpointEntryKind::DataBreakpoint(data_breakpoint) => { let id = data_breakpoint.0.dap.data_id.clone(); self.toggle_data_breakpoint(&id, cx); } } cx.notify(); } fn unset_breakpoint( &mut self, _: &UnsetBreakpoint, _window: &mut Window, cx: &mut Context, ) { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind { let path = line_breakpoint.breakpoint.path.clone(); let row = line_breakpoint.breakpoint.row; self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); } cx.notify(); } fn previous_breakpoint_property( &mut self, _: &PreviousBreakpointProperty, window: &mut Window, cx: &mut Context, ) { let next_mode = match self.strip_mode { Some(ActiveBreakpointStripMode::Log) => None, Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log), Some(ActiveBreakpointStripMode::HitCondition) => { Some(ActiveBreakpointStripMode::Condition) } None => Some(ActiveBreakpointStripMode::HitCondition), }; if let Some(mode) = next_mode { self.set_active_breakpoint_property(mode, window, cx); } else { self.strip_mode.take(); } cx.notify(); } fn next_breakpoint_property( &mut self, _: &NextBreakpointProperty, window: &mut Window, cx: &mut Context, ) { let next_mode = match self.strip_mode { Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition), Some(ActiveBreakpointStripMode::Condition) => { Some(ActiveBreakpointStripMode::HitCondition) } Some(ActiveBreakpointStripMode::HitCondition) => None, None => Some(ActiveBreakpointStripMode::Log), }; if let Some(mode) = next_mode { self.set_active_breakpoint_property(mode, window, cx); } else { self.strip_mode.take(); } cx.notify(); } fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { this.toggle_data_breakpoint(id, cx); }); } } fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { this.toggle_exception_breakpoint(id, cx); }); cx.notify(); const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1); self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| { cx.background_executor() .timer(EXCEPTION_SERIALIZATION_INTERVAL) .await; this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))? .await?; Ok(()) })); } } fn kvp_key(adapter_name: &str) -> String { format!("debug_adapter_`{adapter_name}`_persistence") } fn serialize_exception_breakpoints( &mut self, cx: &mut Context, ) -> Task> { if let Some(session) = self.session.as_ref() { let key = { let session = session.read(cx); let name = session.adapter().0; Self::kvp_key(&name) }; let settings = self.dap_store.update(cx, |this, cx| { this.sync_adapter_options(session, cx); }); let value = serde_json::to_string(&settings); let kvp = KeyValueStore::global(cx); cx.background_executor() .spawn(async move { kvp.write_kvp(key, value?).await }) } else { Task::ready(Result::Ok(())) } } fn deserialize_exception_breakpoints( &self, adapter_name: DebugAdapterName, cx: &mut Context, ) -> anyhow::Result<()> { let Some(val) = KeyValueStore::global(cx).read_kvp(&Self::kvp_key(&adapter_name))? else { return Ok(()); }; let value: PersistedAdapterOptions = serde_json::from_str(&val)?; self.dap_store .update(cx, |this, _| this.set_adapter_options(adapter_name, value)); Ok(()) } fn render_list(&mut self, cx: &mut Context) -> impl IntoElement { let selected_ix = self.selected_ix; let focus_handle = self.focus_handle.clone(); let supported_breakpoint_properties = self .session .as_ref() .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities())) .unwrap_or_else(SupportedBreakpointProperties::all); let strip_mode = self.strip_mode; uniform_list( "breakpoint-list", self.breakpoints.len(), cx.processor(move |this, range: Range, _, _| { range .clone() .zip(&mut this.breakpoints[range]) .map(|(ix, breakpoint)| { breakpoint .render( strip_mode, supported_breakpoint_properties, ix, Some(ix) == selected_ix, focus_handle.clone(), ) .into_any_element() }) .collect() }), ) .with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained) .with_width_from_item(self.max_width_index) .track_scroll(&self.scroll_handle) .flex_1() } pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind { SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list", SelectedBreakpointKind::Exception => { "Exception Breakpoints cannot be removed from the breakpoint list" } SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list", }); let toggle_label = selection_kind.map(|(_, is_enabled)| { if is_enabled { ( "Disable Breakpoint", "Disable a breakpoint without removing it from the list", ) } else { ("Enable Breakpoint", "Re-enable a breakpoint") } }); h_flex() .child( IconButton::new( "disable-breakpoint-breakpoint-list", IconName::DebugDisabledBreakpoint, ) .icon_size(IconSize::Small) .when_some(toggle_label, |this, (label, meta)| { this.tooltip({ let focus_handle = focus_handle.clone(); move |_window, cx| { Tooltip::with_meta_in( label, Some(&ToggleEnableBreakpoint), meta, &focus_handle, cx, ) } }) }) .disabled(selection_kind.is_none()) .on_click({ let focus_handle = focus_handle.clone(); move |_, window, cx| { focus_handle.focus(window, cx); window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx) } }), ) .child( IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash) .icon_size(IconSize::Small) .when_some(remove_breakpoint_tooltip, |this, tooltip| { this.tooltip({ let focus_handle = focus_handle.clone(); move |_window, cx| { Tooltip::with_meta_in( "Remove Breakpoint", Some(&UnsetBreakpoint), tooltip, &focus_handle, cx, ) } }) }) .disabled( selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source), ) .on_click({ move |_, window, cx| { focus_handle.focus(window, cx); window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx) } }), ) .into_any_element() } } impl Render for BreakpointList { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx); self.breakpoints.clear(); let path_style = self.worktree_store.read(cx).path_style(); 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(|| 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 breakpoint_path = RelPath::new(&breakpoint.path, path_style).ok(); let dir = relative_worktree_path .as_deref() .or(breakpoint_path.as_deref())? .parent() .map(|parent| SharedString::from(parent.display(path_style).to_string())); let name = file_name .to_str() .map(ToOwned::to_owned) .map(SharedString::from)?; let weak = weak.clone(); let line = breakpoint.row + 1; Some(BreakpointEntry { kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint { name, dir, line, breakpoint, }), weak, }) }) }); let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| { 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(), }) }); let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| { session .read(cx) .data_breakpoints() .map(|state| BreakpointEntry { kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())), weak: weak.clone(), }) }); self.breakpoints.extend( breakpoints .chain(data_breakpoints) .chain(exception_breakpoints), ); let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32; self.max_width_index = self .breakpoints .iter() .map(|entry| match &entry.kind { BreakpointEntryKind::LineBreakpoint(line_bp) => { let name_and_line = format!("{}:{}", line_bp.name, line_bp.line); let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0); (name_and_line.len() + dir_len) as f32 * text_pixels } BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => { exc_bp.data.label.len() as f32 * text_pixels } BreakpointEntryKind::DataBreakpoint(data_bp) => { data_bp.0.context.human_readable_label().len() as f32 * text_pixels } }) .position_max_by(|left, right| left.total_cmp(right)); v_flex() .id("breakpoint-list") .key_context("BreakpointList") .track_focus(&self.focus_handle) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::toggle_enable_breakpoint)) .on_action(cx.listener(Self::unset_breakpoint)) .on_action(cx.listener(Self::next_breakpoint_property)) .on_action(cx.listener(Self::previous_breakpoint_property)) .size_full() .pt_1() .child(self.render_list(cx)) .custom_scrollbars( ui::Scrollbars::new(ScrollAxes::Both) .tracked_scroll_handle(&self.scroll_handle) .with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background) .tracked_entity(cx.entity_id()), window, cx, ) .when_some(self.strip_mode, |this, _| { this.child(Divider::horizontal().color(DividerColor::Border)) .child( h_flex() .p_1() .rounded_sm() .bg(cx.theme().colors().editor_background) .border_1() .when( self.input.focus_handle(cx).contains_focused(window, cx), |this| { let colors = cx.theme().colors(); let border_color = if self.input.read(cx).read_only(cx) { colors.border_disabled } else { colors.border_transparent }; this.border_color(border_color) }, ) .child(self.input.clone()), ) }) } } #[derive(Clone, Debug)] struct LineBreakpoint { name: SharedString, dir: Option, line: u32, breakpoint: SourceBreakpoint, } impl LineBreakpoint { fn render( &mut self, props: SupportedBreakpointProperties, strip_mode: Option, ix: usize, is_selected: bool, focus_handle: FocusHandle, weak: WeakEntity, ) -> ListItem { let icon_name = if self.breakpoint.state.is_enabled() { IconName::DebugBreakpoint } else { IconName::DebugDisabledBreakpoint }; let path = self.breakpoint.path.clone(); let row = self.breakpoint.row; let is_enabled = self.breakpoint.state.is_enabled(); let indicator = div() .id(SharedString::from(format!( "breakpoint-ui-toggle-{:?}/{}:{}", self.dir, self.name, self.line ))) .child( Icon::new(icon_name) .color(Color::Debugger) .size(IconSize::XSmall), ) .tooltip({ let focus_handle = focus_handle.clone(); move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Breakpoint" } else { "Enable Breakpoint" }, &ToggleEnableBreakpoint, &focus_handle, cx, ) } }) .on_click({ let weak = weak.clone(); let path = path.clone(); move |_, _, cx| { weak.update(cx, |breakpoint_list, cx| { breakpoint_list.edit_line_breakpoint( path.clone(), row, BreakpointEditAction::InvertState, cx, ); }) .ok(); } }) .on_mouse_down(MouseButton::Left, move |_, _, _| {}); ListItem::new(SharedString::from(format!( "breakpoint-ui-item-{:?}/{}:{}", self.dir, self.name, self.line ))) .toggle_state(is_selected) .inset(true) .on_click({ let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |breakpoint_list, cx| { breakpoint_list.select_ix(Some(ix), window, cx); }) .ok(); } }) .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) .start_slot(indicator) .child( h_flex() .id(SharedString::from(format!( "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", self.dir, self.name, self.line ))) .w_full() .gap_1() .min_h(rems_from_px(26.)) .justify_between() .on_click({ let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |breakpoint_list, cx| { breakpoint_list.select_ix(Some(ix), window, cx); breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx); }) .ok(); } }) .child( h_flex() .id("label-container") .gap_0p5() .child( Label::new(format!("{}:{}", self.name, self.line)) .size(LabelSize::Small) .line_height_style(ui::LineHeightStyle::UiLabel), ) .children(self.dir.as_ref().and_then(|dir| { let path_without_root = Path::new(dir.as_ref()) .components() .skip(1) .collect::(); path_without_root.components().next()?; Some( Label::new(path_without_root.to_string_lossy().into_owned()) .color(Color::Muted) .size(LabelSize::Small) .line_height_style(ui::LineHeightStyle::UiLabel) .truncate(), ) })) .when_some(self.dir.as_ref(), |this, parent_dir| { this.tooltip(Tooltip::text(format!( "Worktree parent path: {parent_dir}" ))) }), ) .child(BreakpointOptionsStrip { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::LineBreakpoint(self.clone()), weak, }, is_selected, focus_handle, strip_mode, index: ix, }), ) } } #[derive(Clone, Debug)] struct ExceptionBreakpoint { id: String, data: ExceptionBreakpointsFilter, is_enabled: bool, } #[derive(Clone, Debug)] struct DataBreakpoint(project::debugger::session::DataBreakpointState); impl DataBreakpoint { fn render( &self, props: SupportedBreakpointProperties, strip_mode: Option, ix: usize, is_selected: bool, focus_handle: FocusHandle, list: WeakEntity, ) -> ListItem { let color = if self.0.is_enabled { Color::Debugger } else { Color::Muted }; let is_enabled = self.0.is_enabled; let id = self.0.dap.data_id.clone(); ListItem::new(SharedString::from(format!( "data-breakpoint-ui-item-{}", self.0.dap.data_id ))) .toggle_state(is_selected) .inset(true) .start_slot( div() .id(SharedString::from(format!( "data-breakpoint-ui-item-{}-click-handler", self.0.dap.data_id ))) .child( Icon::new(IconName::Binary) .color(color) .size(IconSize::Small), ) .tooltip({ let focus_handle = focus_handle.clone(); move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Data Breakpoint" } else { "Enable Data Breakpoint" }, &ToggleEnableBreakpoint, &focus_handle, cx, ) } }) .on_click({ let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_data_breakpoint(&id, cx); }) .ok(); } }), ) .child( h_flex() .w_full() .gap_1() .min_h(rems_from_px(26.)) .justify_between() .child( v_flex() .py_1() .gap_1() .justify_center() .id(("data-breakpoint-label", ix)) .child( Label::new(self.0.context.human_readable_label()) .size(LabelSize::Small) .line_height_style(ui::LineHeightStyle::UiLabel), ), ) .child(BreakpointOptionsStrip { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::DataBreakpoint(self.clone()), weak: list, }, is_selected, focus_handle, strip_mode, index: ix, }), ) } } impl ExceptionBreakpoint { fn render( &mut self, props: SupportedBreakpointProperties, strip_mode: Option, ix: usize, is_selected: bool, focus_handle: FocusHandle, list: WeakEntity, ) -> ListItem { let color = if self.is_enabled { Color::Debugger } else { Color::Muted }; let id = SharedString::from(&self.id); let is_enabled = self.is_enabled; let weak = list.clone(); ListItem::new(SharedString::from(format!( "exception-breakpoint-ui-item-{}", self.id ))) .toggle_state(is_selected) .inset(true) .on_click({ let list = list.clone(); move |_, window, cx| { list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx)) .ok(); } }) .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) .start_slot( div() .id(SharedString::from(format!( "exception-breakpoint-ui-item-{}-click-handler", self.id ))) .child( Icon::new(IconName::Flame) .color(color) .size(IconSize::Small), ) .tooltip({ let focus_handle = focus_handle.clone(); move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Exception Breakpoint" } else { "Enable Exception Breakpoint" }, &ToggleEnableBreakpoint, &focus_handle, cx, ) } }) .on_click({ move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_exception_breakpoint(&id, cx); }) .ok(); } }), ) .child( h_flex() .w_full() .gap_1() .min_h(rems_from_px(26.)) .justify_between() .child( v_flex() .py_1() .gap_1() .justify_center() .id(("exception-breakpoint-label", ix)) .child( Label::new(self.data.label.clone()) .size(LabelSize::Small) .line_height_style(ui::LineHeightStyle::UiLabel), ) .when_some(self.data.description.clone(), |el, description| { el.tooltip(Tooltip::text(description)) }), ) .child(BreakpointOptionsStrip { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()), weak, }, is_selected, focus_handle, strip_mode, index: ix, }), ) } } #[derive(Clone, Debug)] enum BreakpointEntryKind { LineBreakpoint(LineBreakpoint), ExceptionBreakpoint(ExceptionBreakpoint), DataBreakpoint(DataBreakpoint), } #[derive(Clone, Debug)] struct BreakpointEntry { kind: BreakpointEntryKind, weak: WeakEntity, } impl BreakpointEntry { fn render( &mut self, strip_mode: Option, props: SupportedBreakpointProperties, ix: usize, is_selected: bool, focus_handle: FocusHandle, ) -> ListItem { match &mut self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render( props, strip_mode, ix, is_selected, focus_handle, self.weak.clone(), ), BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint .render( props.for_exception_breakpoints(), strip_mode, ix, is_selected, focus_handle, self.weak.clone(), ), BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render( props.for_data_breakpoints(), strip_mode, ix, is_selected, focus_handle, self.weak.clone(), ), } } fn id(&self) -> SharedString { match &self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!( "source-breakpoint-control-strip-{:?}:{}", line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row ) .into(), BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!( "exception-breakpoint-control-strip--{}", exception_breakpoint.id ) .into(), BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!( "data-breakpoint-control-strip--{}", data_breakpoint.0.dap.data_id ) .into(), } } fn has_log(&self) -> bool { match &self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { line_breakpoint.breakpoint.message.is_some() } _ => false, } } fn has_condition(&self) -> bool { match &self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { line_breakpoint.breakpoint.condition.is_some() } // We don't support conditions on exception/data breakpoints _ => false, } } fn has_hit_condition(&self) -> bool { match &self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { line_breakpoint.breakpoint.hit_condition.is_some() } _ => false, } } } bitflags::bitflags! { #[derive(Clone, Copy)] pub struct SupportedBreakpointProperties: u32 { const LOG = 1 << 0; const CONDITION = 1 << 1; const HIT_CONDITION = 1 << 2; // Conditions for exceptions can be set only when exception filters are supported. const EXCEPTION_FILTER_OPTIONS = 1 << 3; } } impl From<&Capabilities> for SupportedBreakpointProperties { fn from(caps: &Capabilities) -> Self { let mut this = Self::empty(); for (prop, offset) in [ (caps.supports_log_points, Self::LOG), (caps.supports_conditional_breakpoints, Self::CONDITION), ( caps.supports_hit_conditional_breakpoints, Self::HIT_CONDITION, ), ( caps.supports_exception_options, Self::EXCEPTION_FILTER_OPTIONS, ), ] { if prop.unwrap_or_default() { this.insert(offset); } } this } } impl SupportedBreakpointProperties { fn for_exception_breakpoints(self) -> Self { // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here. Self::empty() } fn for_data_breakpoints(self) -> Self { // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here. Self::empty() } } #[derive(IntoElement)] struct BreakpointOptionsStrip { props: SupportedBreakpointProperties, breakpoint: BreakpointEntry, is_selected: bool, focus_handle: FocusHandle, strip_mode: Option, index: usize, } impl BreakpointOptionsStrip { fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool { self.is_selected && self.strip_mode == Some(expected_mode) } fn on_click_callback( &self, mode: ActiveBreakpointStripMode, ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> { let list = self.breakpoint.weak.clone(); let ix = self.index; move |_, window, cx| { list.update(cx, |this, cx| { if this.strip_mode != Some(mode) { this.set_active_breakpoint_property(mode, window, cx); } else if this.selected_ix == Some(ix) { this.strip_mode.take(); } else { cx.propagate(); } }) .ok(); } } fn add_focus_styles( &self, kind: ActiveBreakpointStripMode, available: bool, window: &Window, cx: &App, ) -> impl Fn(Div) -> Div { move |this: Div| { // Avoid layout shifts in case there's no colored border let this = this.border_1().rounded_sm(); let color = cx.theme().colors(); if self.is_selected && self.strip_mode == Some(kind) { if self.focus_handle.is_focused(window) { this.bg(color.editor_background) .border_color(color.border_focused) } else { this.border_color(color.border) } } else if !available { this.border_color(color.border_transparent) } else { this } } } } impl RenderOnce for BreakpointOptionsStrip { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.breakpoint.id(); let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG); let supports_condition = self .props .contains(SupportedBreakpointProperties::CONDITION); let supports_hit_condition = self .props .contains(SupportedBreakpointProperties::HIT_CONDITION); let has_logs = self.breakpoint.has_log(); let has_condition = self.breakpoint.has_condition(); let has_hit_condition = self.breakpoint.has_hit_condition(); let style_for_toggle = |mode, is_enabled| { if is_enabled && self.strip_mode == Some(mode) && self.is_selected { ui::ButtonStyle::Filled } else { ui::ButtonStyle::Subtle } }; let color_for_toggle = |is_enabled| { if is_enabled { Color::Default } else { Color::Muted } }; h_flex() .gap_px() .justify_end() .when(has_logs || self.is_selected, |this| { this.child( div() .map(self.add_focus_styles( ActiveBreakpointStripMode::Log, supports_logs, window, cx, )) .child( IconButton::new( SharedString::from(format!("{id}-log-toggle")), IconName::Notepad, ) .shape(ui::IconButtonShape::Square) .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs)) .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_logs)) .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_logs) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)) .tooltip(|_window, cx| { Tooltip::with_meta( "Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit.", cx, ) }), ) ) }) .when(has_condition || self.is_selected, |this| { this.child( div() .map(self.add_focus_styles( ActiveBreakpointStripMode::Condition, supports_condition, window, cx, )) .child( IconButton::new( SharedString::from(format!("{id}-condition-toggle")), IconName::SplitAlt, ) .shape(ui::IconButtonShape::Square) .style(style_for_toggle( ActiveBreakpointStripMode::Condition, has_condition, )) .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_condition)) .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition)) .tooltip(|_window, cx| { Tooltip::with_meta( "Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.", cx, ) }), ) ) }) .when(has_hit_condition || self.is_selected, |this| { this.child(div() .map(self.add_focus_styles( ActiveBreakpointStripMode::HitCondition, supports_hit_condition, window, cx, )) .child( IconButton::new( SharedString::from(format!("{id}-hit-condition-toggle")), IconName::ArrowDown10, ) .style(style_for_toggle( ActiveBreakpointStripMode::HitCondition, has_hit_condition, )) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_hit_condition)) .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_hit_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)) .tooltip(|_window, cx| { Tooltip::with_meta( "Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", cx, ) }), )) }) } }