breakpoint_list.rs

   1use std::{
   2    ops::Range,
   3    path::{Path, PathBuf},
   4    sync::Arc,
   5    time::Duration,
   6};
   7
   8use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
   9use db::kvp::KEY_VALUE_STORE;
  10use editor::Editor;
  11use gpui::{
  12    Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
  13    Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
  14};
  15use language::Point;
  16use project::{
  17    Project,
  18    debugger::{
  19        breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
  20        dap_store::{DapStore, PersistedAdapterOptions},
  21        session::Session,
  22    },
  23    worktree_store::WorktreeStore,
  24};
  25use ui::{
  26    Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render,
  27    StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
  28};
  29use util::rel_path::RelPath;
  30use workspace::Workspace;
  31use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
  32
  33actions!(
  34    debugger,
  35    [
  36        /// Navigates to the previous breakpoint property in the list.
  37        PreviousBreakpointProperty,
  38        /// Navigates to the next breakpoint property in the list.
  39        NextBreakpointProperty
  40    ]
  41);
  42#[derive(Clone, Copy, PartialEq)]
  43pub(crate) enum SelectedBreakpointKind {
  44    Source,
  45    Exception,
  46    Data,
  47}
  48pub(crate) struct BreakpointList {
  49    workspace: WeakEntity<Workspace>,
  50    breakpoint_store: Entity<BreakpointStore>,
  51    dap_store: Entity<DapStore>,
  52    worktree_store: Entity<WorktreeStore>,
  53    breakpoints: Vec<BreakpointEntry>,
  54    session: Option<Entity<Session>>,
  55    focus_handle: FocusHandle,
  56    scroll_handle: UniformListScrollHandle,
  57    selected_ix: Option<usize>,
  58    input: Entity<Editor>,
  59    strip_mode: Option<ActiveBreakpointStripMode>,
  60    serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
  61}
  62
  63impl Focusable for BreakpointList {
  64    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
  65        self.focus_handle.clone()
  66    }
  67}
  68
  69#[derive(Clone, Copy, PartialEq)]
  70enum ActiveBreakpointStripMode {
  71    Log,
  72    Condition,
  73    HitCondition,
  74}
  75
  76impl BreakpointList {
  77    pub(crate) fn new(
  78        session: Option<Entity<Session>>,
  79        workspace: WeakEntity<Workspace>,
  80        project: &Entity<Project>,
  81        window: &mut Window,
  82        cx: &mut App,
  83    ) -> Entity<Self> {
  84        let project = project.read(cx);
  85        let breakpoint_store = project.breakpoint_store();
  86        let worktree_store = project.worktree_store();
  87        let dap_store = project.dap_store();
  88        let focus_handle = cx.focus_handle();
  89        let scroll_handle = UniformListScrollHandle::new();
  90
  91        let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
  92        cx.new(|cx| {
  93            let this = Self {
  94                breakpoint_store,
  95                dap_store,
  96                worktree_store,
  97                breakpoints: Default::default(),
  98                workspace,
  99                session,
 100                focus_handle,
 101                scroll_handle,
 102                selected_ix: None,
 103                input: cx.new(|cx| Editor::single_line(window, cx)),
 104                strip_mode: None,
 105                serialize_exception_breakpoints_task: None,
 106            };
 107            if let Some(name) = adapter_name {
 108                _ = this.deserialize_exception_breakpoints(name, cx);
 109            }
 110            this
 111        })
 112    }
 113
 114    fn edit_line_breakpoint(
 115        &self,
 116        path: Arc<Path>,
 117        row: u32,
 118        action: BreakpointEditAction,
 119        cx: &mut App,
 120    ) {
 121        Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
 122    }
 123    fn edit_line_breakpoint_inner(
 124        breakpoint_store: &Entity<BreakpointStore>,
 125        path: Arc<Path>,
 126        row: u32,
 127        action: BreakpointEditAction,
 128        cx: &mut App,
 129    ) {
 130        breakpoint_store.update(cx, |breakpoint_store, cx| {
 131            if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
 132                breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
 133            } else {
 134                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
 135            }
 136        })
 137    }
 138
 139    fn go_to_line_breakpoint(
 140        &mut self,
 141        path: Arc<Path>,
 142        row: u32,
 143        window: &mut Window,
 144        cx: &mut Context<Self>,
 145    ) {
 146        let task = self
 147            .worktree_store
 148            .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
 149        cx.spawn_in(window, async move |this, cx| {
 150            let (worktree, relative_path) = task.await?;
 151            let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
 152            let item = this
 153                .update_in(cx, |this, window, cx| {
 154                    this.workspace.update(cx, |this, cx| {
 155                        this.open_path((worktree_id, relative_path), None, true, window, cx)
 156                    })
 157                })??
 158                .await?;
 159            if let Some(editor) = item.downcast::<Editor>() {
 160                editor
 161                    .update_in(cx, |this, window, cx| {
 162                        this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
 163                    })
 164                    .ok();
 165            }
 166            anyhow::Ok(())
 167        })
 168        .detach();
 169    }
 170
 171    pub(crate) fn selection_kind(&self) -> Option<(SelectedBreakpointKind, bool)> {
 172        self.selected_ix.and_then(|ix| {
 173            self.breakpoints.get(ix).map(|bp| match &bp.kind {
 174                BreakpointEntryKind::LineBreakpoint(bp) => (
 175                    SelectedBreakpointKind::Source,
 176                    bp.breakpoint.state
 177                        == project::debugger::breakpoint_store::BreakpointState::Enabled,
 178                ),
 179                BreakpointEntryKind::ExceptionBreakpoint(bp) => {
 180                    (SelectedBreakpointKind::Exception, bp.is_enabled)
 181                }
 182                BreakpointEntryKind::DataBreakpoint(bp) => {
 183                    (SelectedBreakpointKind::Data, bp.0.is_enabled)
 184                }
 185            })
 186        })
 187    }
 188
 189    fn set_active_breakpoint_property(
 190        &mut self,
 191        prop: ActiveBreakpointStripMode,
 192        window: &mut Window,
 193        cx: &mut App,
 194    ) {
 195        self.strip_mode = Some(prop);
 196        let placeholder = match prop {
 197            ActiveBreakpointStripMode::Log => "Set Log Message",
 198            ActiveBreakpointStripMode::Condition => "Set Condition",
 199            ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
 200        };
 201        let mut is_exception_breakpoint = true;
 202        let active_value = self.selected_ix.and_then(|ix| {
 203            self.breakpoints.get(ix).and_then(|bp| {
 204                if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
 205                    is_exception_breakpoint = false;
 206                    match prop {
 207                        ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
 208                        ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
 209                        ActiveBreakpointStripMode::HitCondition => {
 210                            bp.breakpoint.hit_condition.clone()
 211                        }
 212                    }
 213                } else {
 214                    None
 215                }
 216            })
 217        });
 218
 219        self.input.update(cx, |this, cx| {
 220            this.set_placeholder_text(placeholder, window, cx);
 221            this.set_read_only(is_exception_breakpoint);
 222            this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
 223        });
 224    }
 225
 226    fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
 227        self.selected_ix = ix;
 228        if let Some(ix) = ix {
 229            self.scroll_handle
 230                .scroll_to_item(ix, ScrollStrategy::Center);
 231        }
 232        if let Some(mode) = self.strip_mode {
 233            self.set_active_breakpoint_property(mode, window, cx);
 234        }
 235
 236        cx.notify();
 237    }
 238
 239    fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 240        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
 241            cx.propagate();
 242            return;
 243        }
 244        let ix = match self.selected_ix {
 245            _ if self.breakpoints.is_empty() => None,
 246            None => Some(0),
 247            Some(ix) => {
 248                if ix == self.breakpoints.len() - 1 {
 249                    Some(0)
 250                } else {
 251                    Some(ix + 1)
 252                }
 253            }
 254        };
 255        self.select_ix(ix, window, cx);
 256    }
 257
 258    fn select_previous(
 259        &mut self,
 260        _: &menu::SelectPrevious,
 261        window: &mut Window,
 262        cx: &mut Context<Self>,
 263    ) {
 264        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
 265            cx.propagate();
 266            return;
 267        }
 268        let ix = match self.selected_ix {
 269            _ if self.breakpoints.is_empty() => None,
 270            None => Some(self.breakpoints.len() - 1),
 271            Some(ix) => {
 272                if ix == 0 {
 273                    Some(self.breakpoints.len() - 1)
 274                } else {
 275                    Some(ix - 1)
 276                }
 277            }
 278        };
 279        self.select_ix(ix, window, cx);
 280    }
 281
 282    fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
 283        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
 284            cx.propagate();
 285            return;
 286        }
 287        let ix = if !self.breakpoints.is_empty() {
 288            Some(0)
 289        } else {
 290            None
 291        };
 292        self.select_ix(ix, window, cx);
 293    }
 294
 295    fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 296        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
 297            cx.propagate();
 298            return;
 299        }
 300        let ix = if !self.breakpoints.is_empty() {
 301            Some(self.breakpoints.len() - 1)
 302        } else {
 303            None
 304        };
 305        self.select_ix(ix, window, cx);
 306    }
 307
 308    fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 309        if self.input.focus_handle(cx).contains_focused(window, cx) {
 310            self.focus_handle.focus(window);
 311        } else if self.strip_mode.is_some() {
 312            self.strip_mode.take();
 313            cx.notify();
 314        } else {
 315            cx.propagate();
 316        }
 317    }
 318    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 319        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
 320            return;
 321        };
 322
 323        if let Some(mode) = self.strip_mode {
 324            let handle = self.input.focus_handle(cx);
 325            if handle.is_focused(window) {
 326                // Go back to the main strip. Save the result as well.
 327                let text = self.input.read(cx).text(cx);
 328
 329                match mode {
 330                    ActiveBreakpointStripMode::Log => {
 331                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
 332                            Self::edit_line_breakpoint_inner(
 333                                &self.breakpoint_store,
 334                                line_breakpoint.breakpoint.path.clone(),
 335                                line_breakpoint.breakpoint.row,
 336                                BreakpointEditAction::EditLogMessage(Arc::from(text)),
 337                                cx,
 338                            );
 339                        }
 340                    }
 341                    ActiveBreakpointStripMode::Condition => {
 342                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
 343                            Self::edit_line_breakpoint_inner(
 344                                &self.breakpoint_store,
 345                                line_breakpoint.breakpoint.path.clone(),
 346                                line_breakpoint.breakpoint.row,
 347                                BreakpointEditAction::EditCondition(Arc::from(text)),
 348                                cx,
 349                            );
 350                        }
 351                    }
 352                    ActiveBreakpointStripMode::HitCondition => {
 353                        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
 354                            Self::edit_line_breakpoint_inner(
 355                                &self.breakpoint_store,
 356                                line_breakpoint.breakpoint.path.clone(),
 357                                line_breakpoint.breakpoint.row,
 358                                BreakpointEditAction::EditHitCondition(Arc::from(text)),
 359                                cx,
 360                            );
 361                        }
 362                    }
 363                }
 364                self.focus_handle.focus(window);
 365            } else {
 366                handle.focus(window);
 367            }
 368
 369            return;
 370        }
 371        match &mut entry.kind {
 372            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
 373                let path = line_breakpoint.breakpoint.path.clone();
 374                let row = line_breakpoint.breakpoint.row;
 375                self.go_to_line_breakpoint(path, row, window, cx);
 376            }
 377            BreakpointEntryKind::DataBreakpoint(_)
 378            | BreakpointEntryKind::ExceptionBreakpoint(_) => {}
 379        }
 380    }
 381
 382    fn toggle_enable_breakpoint(
 383        &mut self,
 384        _: &ToggleEnableBreakpoint,
 385        window: &mut Window,
 386        cx: &mut Context<Self>,
 387    ) {
 388        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
 389            return;
 390        };
 391        if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
 392            cx.propagate();
 393            return;
 394        }
 395
 396        match &mut entry.kind {
 397            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
 398                let path = line_breakpoint.breakpoint.path.clone();
 399                let row = line_breakpoint.breakpoint.row;
 400                self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
 401            }
 402            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
 403                let id = exception_breakpoint.id.clone();
 404                self.toggle_exception_breakpoint(&id, cx);
 405            }
 406            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
 407                let id = data_breakpoint.0.dap.data_id.clone();
 408                self.toggle_data_breakpoint(&id, cx);
 409            }
 410        }
 411        cx.notify();
 412    }
 413
 414    fn unset_breakpoint(
 415        &mut self,
 416        _: &UnsetBreakpoint,
 417        _window: &mut Window,
 418        cx: &mut Context<Self>,
 419    ) {
 420        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
 421            return;
 422        };
 423
 424        if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind {
 425            let path = line_breakpoint.breakpoint.path.clone();
 426            let row = line_breakpoint.breakpoint.row;
 427            self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
 428        }
 429        cx.notify();
 430    }
 431
 432    fn previous_breakpoint_property(
 433        &mut self,
 434        _: &PreviousBreakpointProperty,
 435        window: &mut Window,
 436        cx: &mut Context<Self>,
 437    ) {
 438        let next_mode = match self.strip_mode {
 439            Some(ActiveBreakpointStripMode::Log) => None,
 440            Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
 441            Some(ActiveBreakpointStripMode::HitCondition) => {
 442                Some(ActiveBreakpointStripMode::Condition)
 443            }
 444            None => Some(ActiveBreakpointStripMode::HitCondition),
 445        };
 446        if let Some(mode) = next_mode {
 447            self.set_active_breakpoint_property(mode, window, cx);
 448        } else {
 449            self.strip_mode.take();
 450        }
 451
 452        cx.notify();
 453    }
 454    fn next_breakpoint_property(
 455        &mut self,
 456        _: &NextBreakpointProperty,
 457        window: &mut Window,
 458        cx: &mut Context<Self>,
 459    ) {
 460        let next_mode = match self.strip_mode {
 461            Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
 462            Some(ActiveBreakpointStripMode::Condition) => {
 463                Some(ActiveBreakpointStripMode::HitCondition)
 464            }
 465            Some(ActiveBreakpointStripMode::HitCondition) => None,
 466            None => Some(ActiveBreakpointStripMode::Log),
 467        };
 468        if let Some(mode) = next_mode {
 469            self.set_active_breakpoint_property(mode, window, cx);
 470        } else {
 471            self.strip_mode.take();
 472        }
 473        cx.notify();
 474    }
 475
 476    fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
 477        if let Some(session) = &self.session {
 478            session.update(cx, |this, cx| {
 479                this.toggle_data_breakpoint(id, cx);
 480            });
 481        }
 482    }
 483
 484    fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
 485        if let Some(session) = &self.session {
 486            session.update(cx, |this, cx| {
 487                this.toggle_exception_breakpoint(id, cx);
 488            });
 489            cx.notify();
 490            const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
 491            self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
 492                cx.background_executor()
 493                    .timer(EXCEPTION_SERIALIZATION_INTERVAL)
 494                    .await;
 495                this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
 496                    .await?;
 497                Ok(())
 498            }));
 499        }
 500    }
 501
 502    fn kvp_key(adapter_name: &str) -> String {
 503        format!("debug_adapter_`{adapter_name}`_persistence")
 504    }
 505    fn serialize_exception_breakpoints(
 506        &mut self,
 507        cx: &mut Context<Self>,
 508    ) -> Task<anyhow::Result<()>> {
 509        if let Some(session) = self.session.as_ref() {
 510            let key = {
 511                let session = session.read(cx);
 512                let name = session.adapter().0;
 513                Self::kvp_key(&name)
 514            };
 515            let settings = self.dap_store.update(cx, |this, cx| {
 516                this.sync_adapter_options(session, cx);
 517            });
 518            let value = serde_json::to_string(&settings);
 519
 520            cx.background_executor()
 521                .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
 522        } else {
 523            Task::ready(Result::Ok(()))
 524        }
 525    }
 526
 527    fn deserialize_exception_breakpoints(
 528        &self,
 529        adapter_name: DebugAdapterName,
 530        cx: &mut Context<Self>,
 531    ) -> anyhow::Result<()> {
 532        let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
 533            return Ok(());
 534        };
 535        let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
 536        self.dap_store
 537            .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
 538
 539        Ok(())
 540    }
 541
 542    fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 543        let selected_ix = self.selected_ix;
 544        let focus_handle = self.focus_handle.clone();
 545        let supported_breakpoint_properties = self
 546            .session
 547            .as_ref()
 548            .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
 549            .unwrap_or_else(SupportedBreakpointProperties::empty);
 550        let strip_mode = self.strip_mode;
 551
 552        uniform_list(
 553            "breakpoint-list",
 554            self.breakpoints.len(),
 555            cx.processor(move |this, range: Range<usize>, _, _| {
 556                range
 557                    .clone()
 558                    .zip(&mut this.breakpoints[range])
 559                    .map(|(ix, breakpoint)| {
 560                        breakpoint
 561                            .render(
 562                                strip_mode,
 563                                supported_breakpoint_properties,
 564                                ix,
 565                                Some(ix) == selected_ix,
 566                                focus_handle.clone(),
 567                            )
 568                            .into_any_element()
 569                    })
 570                    .collect()
 571            }),
 572        )
 573        .track_scroll(self.scroll_handle.clone())
 574        .flex_1()
 575    }
 576
 577    pub(crate) fn render_control_strip(&self) -> AnyElement {
 578        let selection_kind = self.selection_kind();
 579        let focus_handle = self.focus_handle.clone();
 580
 581        let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
 582            SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
 583            SelectedBreakpointKind::Exception => {
 584                "Exception Breakpoints cannot be removed from the breakpoint list"
 585            }
 586            SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
 587        });
 588
 589        let toggle_label = selection_kind.map(|(_, is_enabled)| {
 590            if is_enabled {
 591                (
 592                    "Disable Breakpoint",
 593                    "Disable a breakpoint without removing it from the list",
 594                )
 595            } else {
 596                ("Enable Breakpoint", "Re-enable a breakpoint")
 597            }
 598        });
 599
 600        h_flex()
 601            .child(
 602                IconButton::new(
 603                    "disable-breakpoint-breakpoint-list",
 604                    IconName::DebugDisabledBreakpoint,
 605                )
 606                .icon_size(IconSize::Small)
 607                .when_some(toggle_label, |this, (label, meta)| {
 608                    this.tooltip({
 609                        let focus_handle = focus_handle.clone();
 610                        move |window, cx| {
 611                            Tooltip::with_meta_in(
 612                                label,
 613                                Some(&ToggleEnableBreakpoint),
 614                                meta,
 615                                &focus_handle,
 616                                window,
 617                                cx,
 618                            )
 619                        }
 620                    })
 621                })
 622                .disabled(selection_kind.is_none())
 623                .on_click({
 624                    let focus_handle = focus_handle.clone();
 625                    move |_, window, cx| {
 626                        focus_handle.focus(window);
 627                        window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
 628                    }
 629                }),
 630            )
 631            .child(
 632                IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
 633                    .icon_size(IconSize::Small)
 634                    .when_some(remove_breakpoint_tooltip, |this, tooltip| {
 635                        this.tooltip({
 636                            let focus_handle = focus_handle.clone();
 637                            move |window, cx| {
 638                                Tooltip::with_meta_in(
 639                                    "Remove Breakpoint",
 640                                    Some(&UnsetBreakpoint),
 641                                    tooltip,
 642                                    &focus_handle,
 643                                    window,
 644                                    cx,
 645                                )
 646                            }
 647                        })
 648                    })
 649                    .disabled(
 650                        selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
 651                    )
 652                    .on_click({
 653                        move |_, window, cx| {
 654                            focus_handle.focus(window);
 655                            window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
 656                        }
 657                    }),
 658            )
 659            .into_any_element()
 660    }
 661}
 662
 663impl Render for BreakpointList {
 664    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
 665        let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
 666        self.breakpoints.clear();
 667        let path_style = self.worktree_store.read(cx).path_style();
 668        let weak = cx.weak_entity();
 669        let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
 670            let relative_worktree_path = self
 671                .worktree_store
 672                .read(cx)
 673                .find_worktree(&path, cx)
 674                .and_then(|(worktree, relative_path)| {
 675                    worktree
 676                        .read(cx)
 677                        .is_visible()
 678                        .then(|| worktree.read(cx).root_name().join(&relative_path))
 679                });
 680            breakpoints.sort_by_key(|breakpoint| breakpoint.row);
 681            let weak = weak.clone();
 682            breakpoints.into_iter().filter_map(move |breakpoint| {
 683                debug_assert_eq!(&path, &breakpoint.path);
 684                let file_name = breakpoint.path.file_name()?;
 685                let breakpoint_path = RelPath::new(&breakpoint.path, path_style).ok();
 686
 687                let dir = relative_worktree_path
 688                    .as_deref()
 689                    .or(breakpoint_path.as_deref())?
 690                    .parent()
 691                    .map(|parent| SharedString::from(parent.display(path_style).to_string()));
 692                let name = file_name
 693                    .to_str()
 694                    .map(ToOwned::to_owned)
 695                    .map(SharedString::from)?;
 696                let weak = weak.clone();
 697                let line = breakpoint.row + 1;
 698                Some(BreakpointEntry {
 699                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
 700                        name,
 701                        dir,
 702                        line,
 703                        breakpoint,
 704                    }),
 705                    weak,
 706                })
 707            })
 708        });
 709        let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 710            session
 711                .read(cx)
 712                .exception_breakpoints()
 713                .map(|(data, is_enabled)| BreakpointEntry {
 714                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
 715                        id: data.filter.clone(),
 716                        data: data.clone(),
 717                        is_enabled: *is_enabled,
 718                    }),
 719                    weak: weak.clone(),
 720                })
 721        });
 722        let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 723            session
 724                .read(cx)
 725                .data_breakpoints()
 726                .map(|state| BreakpointEntry {
 727                    kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
 728                    weak: weak.clone(),
 729                })
 730        });
 731        self.breakpoints.extend(
 732            breakpoints
 733                .chain(data_breakpoints)
 734                .chain(exception_breakpoints),
 735        );
 736
 737        v_flex()
 738            .id("breakpoint-list")
 739            .key_context("BreakpointList")
 740            .track_focus(&self.focus_handle)
 741            .on_action(cx.listener(Self::select_next))
 742            .on_action(cx.listener(Self::select_previous))
 743            .on_action(cx.listener(Self::select_first))
 744            .on_action(cx.listener(Self::select_last))
 745            .on_action(cx.listener(Self::dismiss))
 746            .on_action(cx.listener(Self::confirm))
 747            .on_action(cx.listener(Self::toggle_enable_breakpoint))
 748            .on_action(cx.listener(Self::unset_breakpoint))
 749            .on_action(cx.listener(Self::next_breakpoint_property))
 750            .on_action(cx.listener(Self::previous_breakpoint_property))
 751            .size_full()
 752            .pt_1()
 753            .child(self.render_list(cx))
 754            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
 755            .when_some(self.strip_mode, |this, _| {
 756                this.child(Divider::horizontal().color(DividerColor::Border))
 757                    .child(
 758                        h_flex()
 759                            .p_1()
 760                            .rounded_sm()
 761                            .bg(cx.theme().colors().editor_background)
 762                            .border_1()
 763                            .when(
 764                                self.input.focus_handle(cx).contains_focused(window, cx),
 765                                |this| {
 766                                    let colors = cx.theme().colors();
 767
 768                                    let border_color = if self.input.read(cx).read_only(cx) {
 769                                        colors.border_disabled
 770                                    } else {
 771                                        colors.border_transparent
 772                                    };
 773
 774                                    this.border_color(border_color)
 775                                },
 776                            )
 777                            .child(self.input.clone()),
 778                    )
 779            })
 780    }
 781}
 782
 783#[derive(Clone, Debug)]
 784struct LineBreakpoint {
 785    name: SharedString,
 786    dir: Option<SharedString>,
 787    line: u32,
 788    breakpoint: SourceBreakpoint,
 789}
 790
 791impl LineBreakpoint {
 792    fn render(
 793        &mut self,
 794        props: SupportedBreakpointProperties,
 795        strip_mode: Option<ActiveBreakpointStripMode>,
 796        ix: usize,
 797        is_selected: bool,
 798        focus_handle: FocusHandle,
 799        weak: WeakEntity<BreakpointList>,
 800    ) -> ListItem {
 801        let icon_name = if self.breakpoint.state.is_enabled() {
 802            IconName::DebugBreakpoint
 803        } else {
 804            IconName::DebugDisabledBreakpoint
 805        };
 806        let path = self.breakpoint.path.clone();
 807        let row = self.breakpoint.row;
 808        let is_enabled = self.breakpoint.state.is_enabled();
 809
 810        let indicator = div()
 811            .id(SharedString::from(format!(
 812                "breakpoint-ui-toggle-{:?}/{}:{}",
 813                self.dir, self.name, self.line
 814            )))
 815            .child(
 816                Icon::new(icon_name)
 817                    .color(Color::Debugger)
 818                    .size(IconSize::XSmall),
 819            )
 820            .tooltip({
 821                let focus_handle = focus_handle.clone();
 822                move |window, cx| {
 823                    Tooltip::for_action_in(
 824                        if is_enabled {
 825                            "Disable Breakpoint"
 826                        } else {
 827                            "Enable Breakpoint"
 828                        },
 829                        &ToggleEnableBreakpoint,
 830                        &focus_handle,
 831                        window,
 832                        cx,
 833                    )
 834                }
 835            })
 836            .on_click({
 837                let weak = weak.clone();
 838                let path = path.clone();
 839                move |_, _, cx| {
 840                    weak.update(cx, |breakpoint_list, cx| {
 841                        breakpoint_list.edit_line_breakpoint(
 842                            path.clone(),
 843                            row,
 844                            BreakpointEditAction::InvertState,
 845                            cx,
 846                        );
 847                    })
 848                    .ok();
 849                }
 850            })
 851            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
 852
 853        ListItem::new(SharedString::from(format!(
 854            "breakpoint-ui-item-{:?}/{}:{}",
 855            self.dir, self.name, self.line
 856        )))
 857        .toggle_state(is_selected)
 858        .inset(true)
 859        .on_click({
 860            let weak = weak.clone();
 861            move |_, window, cx| {
 862                weak.update(cx, |breakpoint_list, cx| {
 863                    breakpoint_list.select_ix(Some(ix), window, cx);
 864                })
 865                .ok();
 866            }
 867        })
 868        .on_secondary_mouse_down(|_, _, cx| {
 869            cx.stop_propagation();
 870        })
 871        .start_slot(indicator)
 872        .child(
 873            h_flex()
 874                .id(SharedString::from(format!(
 875                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
 876                    self.dir, self.name, self.line
 877                )))
 878                .w_full()
 879                .gap_1()
 880                .min_h(rems_from_px(26.))
 881                .justify_between()
 882                .on_click({
 883                    let weak = weak.clone();
 884                    move |_, window, cx| {
 885                        weak.update(cx, |breakpoint_list, cx| {
 886                            breakpoint_list.select_ix(Some(ix), window, cx);
 887                            breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
 888                        })
 889                        .ok();
 890                    }
 891                })
 892                .child(
 893                    h_flex()
 894                        .id("label-container")
 895                        .gap_0p5()
 896                        .child(
 897                            Label::new(format!("{}:{}", self.name, self.line))
 898                                .size(LabelSize::Small)
 899                                .line_height_style(ui::LineHeightStyle::UiLabel),
 900                        )
 901                        .children(self.dir.as_ref().and_then(|dir| {
 902                            let path_without_root = Path::new(dir.as_ref())
 903                                .components()
 904                                .skip(1)
 905                                .collect::<PathBuf>();
 906                            path_without_root.components().next()?;
 907                            Some(
 908                                Label::new(path_without_root.to_string_lossy().into_owned())
 909                                    .color(Color::Muted)
 910                                    .size(LabelSize::Small)
 911                                    .line_height_style(ui::LineHeightStyle::UiLabel)
 912                                    .truncate(),
 913                            )
 914                        }))
 915                        .when_some(self.dir.as_ref(), |this, parent_dir| {
 916                            this.tooltip(Tooltip::text(format!(
 917                                "Worktree parent path: {parent_dir}"
 918                            )))
 919                        }),
 920                )
 921                .child(BreakpointOptionsStrip {
 922                    props,
 923                    breakpoint: BreakpointEntry {
 924                        kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
 925                        weak,
 926                    },
 927                    is_selected,
 928                    focus_handle,
 929                    strip_mode,
 930                    index: ix,
 931                }),
 932        )
 933    }
 934}
 935
 936#[derive(Clone, Debug)]
 937struct ExceptionBreakpoint {
 938    id: String,
 939    data: ExceptionBreakpointsFilter,
 940    is_enabled: bool,
 941}
 942
 943#[derive(Clone, Debug)]
 944struct DataBreakpoint(project::debugger::session::DataBreakpointState);
 945
 946impl DataBreakpoint {
 947    fn render(
 948        &self,
 949        props: SupportedBreakpointProperties,
 950        strip_mode: Option<ActiveBreakpointStripMode>,
 951        ix: usize,
 952        is_selected: bool,
 953        focus_handle: FocusHandle,
 954        list: WeakEntity<BreakpointList>,
 955    ) -> ListItem {
 956        let color = if self.0.is_enabled {
 957            Color::Debugger
 958        } else {
 959            Color::Muted
 960        };
 961        let is_enabled = self.0.is_enabled;
 962        let id = self.0.dap.data_id.clone();
 963
 964        ListItem::new(SharedString::from(format!(
 965            "data-breakpoint-ui-item-{}",
 966            self.0.dap.data_id
 967        )))
 968        .toggle_state(is_selected)
 969        .inset(true)
 970        .start_slot(
 971            div()
 972                .id(SharedString::from(format!(
 973                    "data-breakpoint-ui-item-{}-click-handler",
 974                    self.0.dap.data_id
 975                )))
 976                .child(
 977                    Icon::new(IconName::Binary)
 978                        .color(color)
 979                        .size(IconSize::Small),
 980                )
 981                .tooltip({
 982                    let focus_handle = focus_handle.clone();
 983                    move |window, cx| {
 984                        Tooltip::for_action_in(
 985                            if is_enabled {
 986                                "Disable Data Breakpoint"
 987                            } else {
 988                                "Enable Data Breakpoint"
 989                            },
 990                            &ToggleEnableBreakpoint,
 991                            &focus_handle,
 992                            window,
 993                            cx,
 994                        )
 995                    }
 996                })
 997                .on_click({
 998                    let list = list.clone();
 999                    move |_, _, cx| {
1000                        list.update(cx, |this, cx| {
1001                            this.toggle_data_breakpoint(&id, cx);
1002                        })
1003                        .ok();
1004                    }
1005                }),
1006        )
1007        .child(
1008            h_flex()
1009                .w_full()
1010                .gap_1()
1011                .min_h(rems_from_px(26.))
1012                .justify_between()
1013                .child(
1014                    v_flex()
1015                        .py_1()
1016                        .gap_1()
1017                        .justify_center()
1018                        .id(("data-breakpoint-label", ix))
1019                        .child(
1020                            Label::new(self.0.context.human_readable_label())
1021                                .size(LabelSize::Small)
1022                                .line_height_style(ui::LineHeightStyle::UiLabel),
1023                        ),
1024                )
1025                .child(BreakpointOptionsStrip {
1026                    props,
1027                    breakpoint: BreakpointEntry {
1028                        kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1029                        weak: list,
1030                    },
1031                    is_selected,
1032                    focus_handle,
1033                    strip_mode,
1034                    index: ix,
1035                }),
1036        )
1037    }
1038}
1039
1040impl ExceptionBreakpoint {
1041    fn render(
1042        &mut self,
1043        props: SupportedBreakpointProperties,
1044        strip_mode: Option<ActiveBreakpointStripMode>,
1045        ix: usize,
1046        is_selected: bool,
1047        focus_handle: FocusHandle,
1048        list: WeakEntity<BreakpointList>,
1049    ) -> ListItem {
1050        let color = if self.is_enabled {
1051            Color::Debugger
1052        } else {
1053            Color::Muted
1054        };
1055        let id = SharedString::from(&self.id);
1056        let is_enabled = self.is_enabled;
1057        let weak = list.clone();
1058
1059        ListItem::new(SharedString::from(format!(
1060            "exception-breakpoint-ui-item-{}",
1061            self.id
1062        )))
1063        .toggle_state(is_selected)
1064        .inset(true)
1065        .on_click({
1066            let list = list.clone();
1067            move |_, window, cx| {
1068                list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1069                    .ok();
1070            }
1071        })
1072        .on_secondary_mouse_down(|_, _, cx| {
1073            cx.stop_propagation();
1074        })
1075        .start_slot(
1076            div()
1077                .id(SharedString::from(format!(
1078                    "exception-breakpoint-ui-item-{}-click-handler",
1079                    self.id
1080                )))
1081                .child(
1082                    Icon::new(IconName::Flame)
1083                        .color(color)
1084                        .size(IconSize::Small),
1085                )
1086                .tooltip({
1087                    let focus_handle = focus_handle.clone();
1088                    move |window, cx| {
1089                        Tooltip::for_action_in(
1090                            if is_enabled {
1091                                "Disable Exception Breakpoint"
1092                            } else {
1093                                "Enable Exception Breakpoint"
1094                            },
1095                            &ToggleEnableBreakpoint,
1096                            &focus_handle,
1097                            window,
1098                            cx,
1099                        )
1100                    }
1101                })
1102                .on_click({
1103                    move |_, _, cx| {
1104                        list.update(cx, |this, cx| {
1105                            this.toggle_exception_breakpoint(&id, cx);
1106                        })
1107                        .ok();
1108                    }
1109                }),
1110        )
1111        .child(
1112            h_flex()
1113                .w_full()
1114                .gap_1()
1115                .min_h(rems_from_px(26.))
1116                .justify_between()
1117                .child(
1118                    v_flex()
1119                        .py_1()
1120                        .gap_1()
1121                        .justify_center()
1122                        .id(("exception-breakpoint-label", ix))
1123                        .child(
1124                            Label::new(self.data.label.clone())
1125                                .size(LabelSize::Small)
1126                                .line_height_style(ui::LineHeightStyle::UiLabel),
1127                        )
1128                        .when_some(self.data.description.clone(), |el, description| {
1129                            el.tooltip(Tooltip::text(description))
1130                        }),
1131                )
1132                .child(BreakpointOptionsStrip {
1133                    props,
1134                    breakpoint: BreakpointEntry {
1135                        kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1136                        weak,
1137                    },
1138                    is_selected,
1139                    focus_handle,
1140                    strip_mode,
1141                    index: ix,
1142                }),
1143        )
1144    }
1145}
1146#[derive(Clone, Debug)]
1147enum BreakpointEntryKind {
1148    LineBreakpoint(LineBreakpoint),
1149    ExceptionBreakpoint(ExceptionBreakpoint),
1150    DataBreakpoint(DataBreakpoint),
1151}
1152
1153#[derive(Clone, Debug)]
1154struct BreakpointEntry {
1155    kind: BreakpointEntryKind,
1156    weak: WeakEntity<BreakpointList>,
1157}
1158
1159impl BreakpointEntry {
1160    fn render(
1161        &mut self,
1162        strip_mode: Option<ActiveBreakpointStripMode>,
1163        props: SupportedBreakpointProperties,
1164        ix: usize,
1165        is_selected: bool,
1166        focus_handle: FocusHandle,
1167    ) -> ListItem {
1168        match &mut self.kind {
1169            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1170                props,
1171                strip_mode,
1172                ix,
1173                is_selected,
1174                focus_handle,
1175                self.weak.clone(),
1176            ),
1177            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1178                .render(
1179                    props.for_exception_breakpoints(),
1180                    strip_mode,
1181                    ix,
1182                    is_selected,
1183                    focus_handle,
1184                    self.weak.clone(),
1185                ),
1186            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1187                props.for_data_breakpoints(),
1188                strip_mode,
1189                ix,
1190                is_selected,
1191                focus_handle,
1192                self.weak.clone(),
1193            ),
1194        }
1195    }
1196
1197    fn id(&self) -> SharedString {
1198        match &self.kind {
1199            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1200                "source-breakpoint-control-strip-{:?}:{}",
1201                line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1202            )
1203            .into(),
1204            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1205                "exception-breakpoint-control-strip--{}",
1206                exception_breakpoint.id
1207            )
1208            .into(),
1209            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1210                "data-breakpoint-control-strip--{}",
1211                data_breakpoint.0.dap.data_id
1212            )
1213            .into(),
1214        }
1215    }
1216
1217    const fn has_log(&self) -> bool {
1218        match &self.kind {
1219            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1220                line_breakpoint.breakpoint.message.is_some()
1221            }
1222            _ => false,
1223        }
1224    }
1225
1226    const fn has_condition(&self) -> bool {
1227        match &self.kind {
1228            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1229                line_breakpoint.breakpoint.condition.is_some()
1230            }
1231            // We don't support conditions on exception/data breakpoints
1232            _ => false,
1233        }
1234    }
1235
1236    const fn has_hit_condition(&self) -> bool {
1237        match &self.kind {
1238            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1239                line_breakpoint.breakpoint.hit_condition.is_some()
1240            }
1241            _ => false,
1242        }
1243    }
1244}
1245
1246bitflags::bitflags! {
1247    #[derive(Clone, Copy)]
1248    pub struct SupportedBreakpointProperties: u32 {
1249        const LOG = 1 << 0;
1250        const CONDITION = 1 << 1;
1251        const HIT_CONDITION = 1 << 2;
1252        // Conditions for exceptions can be set only when exception filters are supported.
1253        const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1254    }
1255}
1256
1257impl From<&Capabilities> for SupportedBreakpointProperties {
1258    fn from(caps: &Capabilities) -> Self {
1259        let mut this = Self::empty();
1260        for (prop, offset) in [
1261            (caps.supports_log_points, Self::LOG),
1262            (caps.supports_conditional_breakpoints, Self::CONDITION),
1263            (
1264                caps.supports_hit_conditional_breakpoints,
1265                Self::HIT_CONDITION,
1266            ),
1267            (
1268                caps.supports_exception_options,
1269                Self::EXCEPTION_FILTER_OPTIONS,
1270            ),
1271        ] {
1272            if prop.unwrap_or_default() {
1273                this.insert(offset);
1274            }
1275        }
1276        this
1277    }
1278}
1279
1280impl SupportedBreakpointProperties {
1281    const fn for_exception_breakpoints(self) -> Self {
1282        // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1283        Self::empty()
1284    }
1285    const fn for_data_breakpoints(self) -> Self {
1286        // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1287        Self::empty()
1288    }
1289}
1290#[derive(IntoElement)]
1291struct BreakpointOptionsStrip {
1292    props: SupportedBreakpointProperties,
1293    breakpoint: BreakpointEntry,
1294    is_selected: bool,
1295    focus_handle: FocusHandle,
1296    strip_mode: Option<ActiveBreakpointStripMode>,
1297    index: usize,
1298}
1299
1300impl BreakpointOptionsStrip {
1301    fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1302        self.is_selected && self.strip_mode == Some(expected_mode)
1303    }
1304
1305    fn on_click_callback(
1306        &self,
1307        mode: ActiveBreakpointStripMode,
1308    ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1309        let list = self.breakpoint.weak.clone();
1310        let ix = self.index;
1311        move |_, window, cx| {
1312            list.update(cx, |this, cx| {
1313                if this.strip_mode != Some(mode) {
1314                    this.set_active_breakpoint_property(mode, window, cx);
1315                } else if this.selected_ix == Some(ix) {
1316                    this.strip_mode.take();
1317                } else {
1318                    cx.propagate();
1319                }
1320            })
1321            .ok();
1322        }
1323    }
1324
1325    fn add_focus_styles(
1326        &self,
1327        kind: ActiveBreakpointStripMode,
1328        available: bool,
1329        window: &Window,
1330        cx: &App,
1331    ) -> impl Fn(Div) -> Div {
1332        move |this: Div| {
1333            // Avoid layout shifts in case there's no colored border
1334            let this = this.border_1().rounded_sm();
1335            let color = cx.theme().colors();
1336
1337            if self.is_selected && self.strip_mode == Some(kind) {
1338                if self.focus_handle.is_focused(window) {
1339                    this.bg(color.editor_background)
1340                        .border_color(color.border_focused)
1341                } else {
1342                    this.border_color(color.border)
1343                }
1344            } else if !available {
1345                this.border_color(color.border_transparent)
1346            } else {
1347                this
1348            }
1349        }
1350    }
1351}
1352
1353impl RenderOnce for BreakpointOptionsStrip {
1354    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1355        let id = self.breakpoint.id();
1356        let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1357        let supports_condition = self
1358            .props
1359            .contains(SupportedBreakpointProperties::CONDITION);
1360        let supports_hit_condition = self
1361            .props
1362            .contains(SupportedBreakpointProperties::HIT_CONDITION);
1363        let has_logs = self.breakpoint.has_log();
1364        let has_condition = self.breakpoint.has_condition();
1365        let has_hit_condition = self.breakpoint.has_hit_condition();
1366        let style_for_toggle = |mode, is_enabled| {
1367            if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1368                ui::ButtonStyle::Filled
1369            } else {
1370                ui::ButtonStyle::Subtle
1371            }
1372        };
1373        let color_for_toggle = |is_enabled| {
1374            if is_enabled {
1375                Color::Default
1376            } else {
1377                Color::Muted
1378            }
1379        };
1380
1381        h_flex()
1382            .gap_px()
1383            .mr_3() // Space to avoid overlapping with the scrollbar
1384            .child(
1385                div()
1386                    .map(self.add_focus_styles(
1387                        ActiveBreakpointStripMode::Log,
1388                        supports_logs,
1389                        window,
1390                        cx,
1391                    ))
1392                    .child(
1393                        IconButton::new(
1394                            SharedString::from(format!("{id}-log-toggle")),
1395                            IconName::Notepad,
1396                        )
1397                        .shape(ui::IconButtonShape::Square)
1398                        .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1399                        .icon_size(IconSize::Small)
1400                        .icon_color(color_for_toggle(has_logs))
1401                        .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
1402                        .disabled(!supports_logs)
1403                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1404                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
1405                        .tooltip(|window, cx| {
1406                            Tooltip::with_meta(
1407                                "Set Log Message",
1408                                None,
1409                                "Set log message to display (instead of stopping) when a breakpoint is hit.",
1410                                window,
1411                                cx,
1412                            )
1413                        }),
1414                    )
1415                    .when(!has_logs && !self.is_selected, |this| this.invisible()),
1416            )
1417            .child(
1418                div()
1419                    .map(self.add_focus_styles(
1420                        ActiveBreakpointStripMode::Condition,
1421                        supports_condition,
1422                        window,
1423                        cx,
1424                    ))
1425                    .child(
1426                        IconButton::new(
1427                            SharedString::from(format!("{id}-condition-toggle")),
1428                            IconName::SplitAlt,
1429                        )
1430                        .shape(ui::IconButtonShape::Square)
1431                        .style(style_for_toggle(
1432                            ActiveBreakpointStripMode::Condition,
1433                            has_condition,
1434                        ))
1435                        .icon_size(IconSize::Small)
1436                        .icon_color(color_for_toggle(has_condition))
1437                        .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1438                        .disabled(!supports_condition)
1439                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1440                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1441                        .tooltip(|window, cx| {
1442                            Tooltip::with_meta(
1443                                "Set Condition",
1444                                None,
1445                                "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
1446                                window,
1447                                cx,
1448                            )
1449                        }),
1450                    )
1451                    .when(!has_condition && !self.is_selected, |this| this.invisible()),
1452            )
1453            .child(
1454                div()
1455                    .map(self.add_focus_styles(
1456                        ActiveBreakpointStripMode::HitCondition,
1457                        supports_hit_condition,
1458                        window,
1459                        cx,
1460                    ))
1461                    .child(
1462                        IconButton::new(
1463                            SharedString::from(format!("{id}-hit-condition-toggle")),
1464                            IconName::ArrowDown10,
1465                        )
1466                        .style(style_for_toggle(
1467                            ActiveBreakpointStripMode::HitCondition,
1468                            has_hit_condition,
1469                        ))
1470                        .shape(ui::IconButtonShape::Square)
1471                        .icon_size(IconSize::Small)
1472                        .icon_color(color_for_toggle(has_hit_condition))
1473                        .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1474                        .disabled(!supports_hit_condition)
1475                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1476                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
1477                        .tooltip(|window, cx| {
1478                            Tooltip::with_meta(
1479                                "Set Hit Condition",
1480                                None,
1481                                "Set expression that controls how many hits of the breakpoint are ignored.",
1482                                window,
1483                                cx,
1484                            )
1485                        }),
1486                    )
1487                    .when(!has_hit_condition && !self.is_selected, |this| {
1488                        this.invisible()
1489                    }),
1490            )
1491    }
1492}