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
 686                let dir = relative_worktree_path
 687                    .clone()
 688                    .or_else(|| RelPath::from_std_path(&breakpoint.path, path_style).ok())?
 689                    .parent()
 690                    .map(|parent| SharedString::from(parent.display(path_style).to_string()));
 691                let name = file_name
 692                    .to_str()
 693                    .map(ToOwned::to_owned)
 694                    .map(SharedString::from)?;
 695                let weak = weak.clone();
 696                let line = breakpoint.row + 1;
 697                Some(BreakpointEntry {
 698                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
 699                        name,
 700                        dir,
 701                        line,
 702                        breakpoint,
 703                    }),
 704                    weak,
 705                })
 706            })
 707        });
 708        let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 709            session
 710                .read(cx)
 711                .exception_breakpoints()
 712                .map(|(data, is_enabled)| BreakpointEntry {
 713                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
 714                        id: data.filter.clone(),
 715                        data: data.clone(),
 716                        is_enabled: *is_enabled,
 717                    }),
 718                    weak: weak.clone(),
 719                })
 720        });
 721        let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 722            session
 723                .read(cx)
 724                .data_breakpoints()
 725                .map(|state| BreakpointEntry {
 726                    kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
 727                    weak: weak.clone(),
 728                })
 729        });
 730        self.breakpoints.extend(
 731            breakpoints
 732                .chain(data_breakpoints)
 733                .chain(exception_breakpoints),
 734        );
 735
 736        v_flex()
 737            .id("breakpoint-list")
 738            .key_context("BreakpointList")
 739            .track_focus(&self.focus_handle)
 740            .on_action(cx.listener(Self::select_next))
 741            .on_action(cx.listener(Self::select_previous))
 742            .on_action(cx.listener(Self::select_first))
 743            .on_action(cx.listener(Self::select_last))
 744            .on_action(cx.listener(Self::dismiss))
 745            .on_action(cx.listener(Self::confirm))
 746            .on_action(cx.listener(Self::toggle_enable_breakpoint))
 747            .on_action(cx.listener(Self::unset_breakpoint))
 748            .on_action(cx.listener(Self::next_breakpoint_property))
 749            .on_action(cx.listener(Self::previous_breakpoint_property))
 750            .size_full()
 751            .pt_1()
 752            .child(self.render_list(cx))
 753            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
 754            .when_some(self.strip_mode, |this, _| {
 755                this.child(Divider::horizontal().color(DividerColor::Border))
 756                    .child(
 757                        h_flex()
 758                            .p_1()
 759                            .rounded_sm()
 760                            .bg(cx.theme().colors().editor_background)
 761                            .border_1()
 762                            .when(
 763                                self.input.focus_handle(cx).contains_focused(window, cx),
 764                                |this| {
 765                                    let colors = cx.theme().colors();
 766
 767                                    let border_color = if self.input.read(cx).read_only(cx) {
 768                                        colors.border_disabled
 769                                    } else {
 770                                        colors.border_transparent
 771                                    };
 772
 773                                    this.border_color(border_color)
 774                                },
 775                            )
 776                            .child(self.input.clone()),
 777                    )
 778            })
 779    }
 780}
 781
 782#[derive(Clone, Debug)]
 783struct LineBreakpoint {
 784    name: SharedString,
 785    dir: Option<SharedString>,
 786    line: u32,
 787    breakpoint: SourceBreakpoint,
 788}
 789
 790impl LineBreakpoint {
 791    fn render(
 792        &mut self,
 793        props: SupportedBreakpointProperties,
 794        strip_mode: Option<ActiveBreakpointStripMode>,
 795        ix: usize,
 796        is_selected: bool,
 797        focus_handle: FocusHandle,
 798        weak: WeakEntity<BreakpointList>,
 799    ) -> ListItem {
 800        let icon_name = if self.breakpoint.state.is_enabled() {
 801            IconName::DebugBreakpoint
 802        } else {
 803            IconName::DebugDisabledBreakpoint
 804        };
 805        let path = self.breakpoint.path.clone();
 806        let row = self.breakpoint.row;
 807        let is_enabled = self.breakpoint.state.is_enabled();
 808
 809        let indicator = div()
 810            .id(SharedString::from(format!(
 811                "breakpoint-ui-toggle-{:?}/{}:{}",
 812                self.dir, self.name, self.line
 813            )))
 814            .child(
 815                Icon::new(icon_name)
 816                    .color(Color::Debugger)
 817                    .size(IconSize::XSmall),
 818            )
 819            .tooltip({
 820                let focus_handle = focus_handle.clone();
 821                move |window, cx| {
 822                    Tooltip::for_action_in(
 823                        if is_enabled {
 824                            "Disable Breakpoint"
 825                        } else {
 826                            "Enable Breakpoint"
 827                        },
 828                        &ToggleEnableBreakpoint,
 829                        &focus_handle,
 830                        window,
 831                        cx,
 832                    )
 833                }
 834            })
 835            .on_click({
 836                let weak = weak.clone();
 837                let path = path.clone();
 838                move |_, _, cx| {
 839                    weak.update(cx, |breakpoint_list, cx| {
 840                        breakpoint_list.edit_line_breakpoint(
 841                            path.clone(),
 842                            row,
 843                            BreakpointEditAction::InvertState,
 844                            cx,
 845                        );
 846                    })
 847                    .ok();
 848                }
 849            })
 850            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
 851
 852        ListItem::new(SharedString::from(format!(
 853            "breakpoint-ui-item-{:?}/{}:{}",
 854            self.dir, self.name, self.line
 855        )))
 856        .toggle_state(is_selected)
 857        .inset(true)
 858        .on_click({
 859            let weak = weak.clone();
 860            move |_, window, cx| {
 861                weak.update(cx, |breakpoint_list, cx| {
 862                    breakpoint_list.select_ix(Some(ix), window, cx);
 863                })
 864                .ok();
 865            }
 866        })
 867        .on_secondary_mouse_down(|_, _, cx| {
 868            cx.stop_propagation();
 869        })
 870        .start_slot(indicator)
 871        .child(
 872            h_flex()
 873                .id(SharedString::from(format!(
 874                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
 875                    self.dir, self.name, self.line
 876                )))
 877                .w_full()
 878                .gap_1()
 879                .min_h(rems_from_px(26.))
 880                .justify_between()
 881                .on_click({
 882                    let weak = weak.clone();
 883                    move |_, window, cx| {
 884                        weak.update(cx, |breakpoint_list, cx| {
 885                            breakpoint_list.select_ix(Some(ix), window, cx);
 886                            breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
 887                        })
 888                        .ok();
 889                    }
 890                })
 891                .child(
 892                    h_flex()
 893                        .id("label-container")
 894                        .gap_0p5()
 895                        .child(
 896                            Label::new(format!("{}:{}", self.name, self.line))
 897                                .size(LabelSize::Small)
 898                                .line_height_style(ui::LineHeightStyle::UiLabel),
 899                        )
 900                        .children(self.dir.as_ref().and_then(|dir| {
 901                            let path_without_root = Path::new(dir.as_ref())
 902                                .components()
 903                                .skip(1)
 904                                .collect::<PathBuf>();
 905                            path_without_root.components().next()?;
 906                            Some(
 907                                Label::new(path_without_root.to_string_lossy().into_owned())
 908                                    .color(Color::Muted)
 909                                    .size(LabelSize::Small)
 910                                    .line_height_style(ui::LineHeightStyle::UiLabel)
 911                                    .truncate(),
 912                            )
 913                        }))
 914                        .when_some(self.dir.as_ref(), |this, parent_dir| {
 915                            this.tooltip(Tooltip::text(format!(
 916                                "Worktree parent path: {parent_dir}"
 917                            )))
 918                        }),
 919                )
 920                .child(BreakpointOptionsStrip {
 921                    props,
 922                    breakpoint: BreakpointEntry {
 923                        kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
 924                        weak,
 925                    },
 926                    is_selected,
 927                    focus_handle,
 928                    strip_mode,
 929                    index: ix,
 930                }),
 931        )
 932    }
 933}
 934
 935#[derive(Clone, Debug)]
 936struct ExceptionBreakpoint {
 937    id: String,
 938    data: ExceptionBreakpointsFilter,
 939    is_enabled: bool,
 940}
 941
 942#[derive(Clone, Debug)]
 943struct DataBreakpoint(project::debugger::session::DataBreakpointState);
 944
 945impl DataBreakpoint {
 946    fn render(
 947        &self,
 948        props: SupportedBreakpointProperties,
 949        strip_mode: Option<ActiveBreakpointStripMode>,
 950        ix: usize,
 951        is_selected: bool,
 952        focus_handle: FocusHandle,
 953        list: WeakEntity<BreakpointList>,
 954    ) -> ListItem {
 955        let color = if self.0.is_enabled {
 956            Color::Debugger
 957        } else {
 958            Color::Muted
 959        };
 960        let is_enabled = self.0.is_enabled;
 961        let id = self.0.dap.data_id.clone();
 962
 963        ListItem::new(SharedString::from(format!(
 964            "data-breakpoint-ui-item-{}",
 965            self.0.dap.data_id
 966        )))
 967        .toggle_state(is_selected)
 968        .inset(true)
 969        .start_slot(
 970            div()
 971                .id(SharedString::from(format!(
 972                    "data-breakpoint-ui-item-{}-click-handler",
 973                    self.0.dap.data_id
 974                )))
 975                .child(
 976                    Icon::new(IconName::Binary)
 977                        .color(color)
 978                        .size(IconSize::Small),
 979                )
 980                .tooltip({
 981                    let focus_handle = focus_handle.clone();
 982                    move |window, cx| {
 983                        Tooltip::for_action_in(
 984                            if is_enabled {
 985                                "Disable Data Breakpoint"
 986                            } else {
 987                                "Enable Data Breakpoint"
 988                            },
 989                            &ToggleEnableBreakpoint,
 990                            &focus_handle,
 991                            window,
 992                            cx,
 993                        )
 994                    }
 995                })
 996                .on_click({
 997                    let list = list.clone();
 998                    move |_, _, cx| {
 999                        list.update(cx, |this, cx| {
1000                            this.toggle_data_breakpoint(&id, cx);
1001                        })
1002                        .ok();
1003                    }
1004                }),
1005        )
1006        .child(
1007            h_flex()
1008                .w_full()
1009                .gap_1()
1010                .min_h(rems_from_px(26.))
1011                .justify_between()
1012                .child(
1013                    v_flex()
1014                        .py_1()
1015                        .gap_1()
1016                        .justify_center()
1017                        .id(("data-breakpoint-label", ix))
1018                        .child(
1019                            Label::new(self.0.context.human_readable_label())
1020                                .size(LabelSize::Small)
1021                                .line_height_style(ui::LineHeightStyle::UiLabel),
1022                        ),
1023                )
1024                .child(BreakpointOptionsStrip {
1025                    props,
1026                    breakpoint: BreakpointEntry {
1027                        kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1028                        weak: list,
1029                    },
1030                    is_selected,
1031                    focus_handle,
1032                    strip_mode,
1033                    index: ix,
1034                }),
1035        )
1036    }
1037}
1038
1039impl ExceptionBreakpoint {
1040    fn render(
1041        &mut self,
1042        props: SupportedBreakpointProperties,
1043        strip_mode: Option<ActiveBreakpointStripMode>,
1044        ix: usize,
1045        is_selected: bool,
1046        focus_handle: FocusHandle,
1047        list: WeakEntity<BreakpointList>,
1048    ) -> ListItem {
1049        let color = if self.is_enabled {
1050            Color::Debugger
1051        } else {
1052            Color::Muted
1053        };
1054        let id = SharedString::from(&self.id);
1055        let is_enabled = self.is_enabled;
1056        let weak = list.clone();
1057
1058        ListItem::new(SharedString::from(format!(
1059            "exception-breakpoint-ui-item-{}",
1060            self.id
1061        )))
1062        .toggle_state(is_selected)
1063        .inset(true)
1064        .on_click({
1065            let list = list.clone();
1066            move |_, window, cx| {
1067                list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1068                    .ok();
1069            }
1070        })
1071        .on_secondary_mouse_down(|_, _, cx| {
1072            cx.stop_propagation();
1073        })
1074        .start_slot(
1075            div()
1076                .id(SharedString::from(format!(
1077                    "exception-breakpoint-ui-item-{}-click-handler",
1078                    self.id
1079                )))
1080                .child(
1081                    Icon::new(IconName::Flame)
1082                        .color(color)
1083                        .size(IconSize::Small),
1084                )
1085                .tooltip({
1086                    let focus_handle = focus_handle.clone();
1087                    move |window, cx| {
1088                        Tooltip::for_action_in(
1089                            if is_enabled {
1090                                "Disable Exception Breakpoint"
1091                            } else {
1092                                "Enable Exception Breakpoint"
1093                            },
1094                            &ToggleEnableBreakpoint,
1095                            &focus_handle,
1096                            window,
1097                            cx,
1098                        )
1099                    }
1100                })
1101                .on_click({
1102                    move |_, _, cx| {
1103                        list.update(cx, |this, cx| {
1104                            this.toggle_exception_breakpoint(&id, cx);
1105                        })
1106                        .ok();
1107                    }
1108                }),
1109        )
1110        .child(
1111            h_flex()
1112                .w_full()
1113                .gap_1()
1114                .min_h(rems_from_px(26.))
1115                .justify_between()
1116                .child(
1117                    v_flex()
1118                        .py_1()
1119                        .gap_1()
1120                        .justify_center()
1121                        .id(("exception-breakpoint-label", ix))
1122                        .child(
1123                            Label::new(self.data.label.clone())
1124                                .size(LabelSize::Small)
1125                                .line_height_style(ui::LineHeightStyle::UiLabel),
1126                        )
1127                        .when_some(self.data.description.clone(), |el, description| {
1128                            el.tooltip(Tooltip::text(description))
1129                        }),
1130                )
1131                .child(BreakpointOptionsStrip {
1132                    props,
1133                    breakpoint: BreakpointEntry {
1134                        kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1135                        weak,
1136                    },
1137                    is_selected,
1138                    focus_handle,
1139                    strip_mode,
1140                    index: ix,
1141                }),
1142        )
1143    }
1144}
1145#[derive(Clone, Debug)]
1146enum BreakpointEntryKind {
1147    LineBreakpoint(LineBreakpoint),
1148    ExceptionBreakpoint(ExceptionBreakpoint),
1149    DataBreakpoint(DataBreakpoint),
1150}
1151
1152#[derive(Clone, Debug)]
1153struct BreakpointEntry {
1154    kind: BreakpointEntryKind,
1155    weak: WeakEntity<BreakpointList>,
1156}
1157
1158impl BreakpointEntry {
1159    fn render(
1160        &mut self,
1161        strip_mode: Option<ActiveBreakpointStripMode>,
1162        props: SupportedBreakpointProperties,
1163        ix: usize,
1164        is_selected: bool,
1165        focus_handle: FocusHandle,
1166    ) -> ListItem {
1167        match &mut self.kind {
1168            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1169                props,
1170                strip_mode,
1171                ix,
1172                is_selected,
1173                focus_handle,
1174                self.weak.clone(),
1175            ),
1176            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1177                .render(
1178                    props.for_exception_breakpoints(),
1179                    strip_mode,
1180                    ix,
1181                    is_selected,
1182                    focus_handle,
1183                    self.weak.clone(),
1184                ),
1185            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1186                props.for_data_breakpoints(),
1187                strip_mode,
1188                ix,
1189                is_selected,
1190                focus_handle,
1191                self.weak.clone(),
1192            ),
1193        }
1194    }
1195
1196    fn id(&self) -> SharedString {
1197        match &self.kind {
1198            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1199                "source-breakpoint-control-strip-{:?}:{}",
1200                line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1201            )
1202            .into(),
1203            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1204                "exception-breakpoint-control-strip--{}",
1205                exception_breakpoint.id
1206            )
1207            .into(),
1208            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1209                "data-breakpoint-control-strip--{}",
1210                data_breakpoint.0.dap.data_id
1211            )
1212            .into(),
1213        }
1214    }
1215
1216    fn has_log(&self) -> bool {
1217        match &self.kind {
1218            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1219                line_breakpoint.breakpoint.message.is_some()
1220            }
1221            _ => false,
1222        }
1223    }
1224
1225    fn has_condition(&self) -> bool {
1226        match &self.kind {
1227            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1228                line_breakpoint.breakpoint.condition.is_some()
1229            }
1230            // We don't support conditions on exception/data breakpoints
1231            _ => false,
1232        }
1233    }
1234
1235    fn has_hit_condition(&self) -> bool {
1236        match &self.kind {
1237            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1238                line_breakpoint.breakpoint.hit_condition.is_some()
1239            }
1240            _ => false,
1241        }
1242    }
1243}
1244
1245bitflags::bitflags! {
1246    #[derive(Clone, Copy)]
1247    pub struct SupportedBreakpointProperties: u32 {
1248        const LOG = 1 << 0;
1249        const CONDITION = 1 << 1;
1250        const HIT_CONDITION = 1 << 2;
1251        // Conditions for exceptions can be set only when exception filters are supported.
1252        const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1253    }
1254}
1255
1256impl From<&Capabilities> for SupportedBreakpointProperties {
1257    fn from(caps: &Capabilities) -> Self {
1258        let mut this = Self::empty();
1259        for (prop, offset) in [
1260            (caps.supports_log_points, Self::LOG),
1261            (caps.supports_conditional_breakpoints, Self::CONDITION),
1262            (
1263                caps.supports_hit_conditional_breakpoints,
1264                Self::HIT_CONDITION,
1265            ),
1266            (
1267                caps.supports_exception_options,
1268                Self::EXCEPTION_FILTER_OPTIONS,
1269            ),
1270        ] {
1271            if prop.unwrap_or_default() {
1272                this.insert(offset);
1273            }
1274        }
1275        this
1276    }
1277}
1278
1279impl SupportedBreakpointProperties {
1280    fn for_exception_breakpoints(self) -> Self {
1281        // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1282        Self::empty()
1283    }
1284    fn for_data_breakpoints(self) -> Self {
1285        // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1286        Self::empty()
1287    }
1288}
1289#[derive(IntoElement)]
1290struct BreakpointOptionsStrip {
1291    props: SupportedBreakpointProperties,
1292    breakpoint: BreakpointEntry,
1293    is_selected: bool,
1294    focus_handle: FocusHandle,
1295    strip_mode: Option<ActiveBreakpointStripMode>,
1296    index: usize,
1297}
1298
1299impl BreakpointOptionsStrip {
1300    fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1301        self.is_selected && self.strip_mode == Some(expected_mode)
1302    }
1303
1304    fn on_click_callback(
1305        &self,
1306        mode: ActiveBreakpointStripMode,
1307    ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1308        let list = self.breakpoint.weak.clone();
1309        let ix = self.index;
1310        move |_, window, cx| {
1311            list.update(cx, |this, cx| {
1312                if this.strip_mode != Some(mode) {
1313                    this.set_active_breakpoint_property(mode, window, cx);
1314                } else if this.selected_ix == Some(ix) {
1315                    this.strip_mode.take();
1316                } else {
1317                    cx.propagate();
1318                }
1319            })
1320            .ok();
1321        }
1322    }
1323
1324    fn add_focus_styles(
1325        &self,
1326        kind: ActiveBreakpointStripMode,
1327        available: bool,
1328        window: &Window,
1329        cx: &App,
1330    ) -> impl Fn(Div) -> Div {
1331        move |this: Div| {
1332            // Avoid layout shifts in case there's no colored border
1333            let this = this.border_1().rounded_sm();
1334            let color = cx.theme().colors();
1335
1336            if self.is_selected && self.strip_mode == Some(kind) {
1337                if self.focus_handle.is_focused(window) {
1338                    this.bg(color.editor_background)
1339                        .border_color(color.border_focused)
1340                } else {
1341                    this.border_color(color.border)
1342                }
1343            } else if !available {
1344                this.border_color(color.border_transparent)
1345            } else {
1346                this
1347            }
1348        }
1349    }
1350}
1351
1352impl RenderOnce for BreakpointOptionsStrip {
1353    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1354        let id = self.breakpoint.id();
1355        let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1356        let supports_condition = self
1357            .props
1358            .contains(SupportedBreakpointProperties::CONDITION);
1359        let supports_hit_condition = self
1360            .props
1361            .contains(SupportedBreakpointProperties::HIT_CONDITION);
1362        let has_logs = self.breakpoint.has_log();
1363        let has_condition = self.breakpoint.has_condition();
1364        let has_hit_condition = self.breakpoint.has_hit_condition();
1365        let style_for_toggle = |mode, is_enabled| {
1366            if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1367                ui::ButtonStyle::Filled
1368            } else {
1369                ui::ButtonStyle::Subtle
1370            }
1371        };
1372        let color_for_toggle = |is_enabled| {
1373            if is_enabled {
1374                Color::Default
1375            } else {
1376                Color::Muted
1377            }
1378        };
1379
1380        h_flex()
1381            .gap_px()
1382            .mr_3() // Space to avoid overlapping with the scrollbar
1383            .child(
1384                div()
1385                    .map(self.add_focus_styles(
1386                        ActiveBreakpointStripMode::Log,
1387                        supports_logs,
1388                        window,
1389                        cx,
1390                    ))
1391                    .child(
1392                        IconButton::new(
1393                            SharedString::from(format!("{id}-log-toggle")),
1394                            IconName::Notepad,
1395                        )
1396                        .shape(ui::IconButtonShape::Square)
1397                        .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1398                        .icon_size(IconSize::Small)
1399                        .icon_color(color_for_toggle(has_logs))
1400                        .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
1401                        .disabled(!supports_logs)
1402                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1403                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
1404                        .tooltip(|window, cx| {
1405                            Tooltip::with_meta(
1406                                "Set Log Message",
1407                                None,
1408                                "Set log message to display (instead of stopping) when a breakpoint is hit.",
1409                                window,
1410                                cx,
1411                            )
1412                        }),
1413                    )
1414                    .when(!has_logs && !self.is_selected, |this| this.invisible()),
1415            )
1416            .child(
1417                div()
1418                    .map(self.add_focus_styles(
1419                        ActiveBreakpointStripMode::Condition,
1420                        supports_condition,
1421                        window,
1422                        cx,
1423                    ))
1424                    .child(
1425                        IconButton::new(
1426                            SharedString::from(format!("{id}-condition-toggle")),
1427                            IconName::SplitAlt,
1428                        )
1429                        .shape(ui::IconButtonShape::Square)
1430                        .style(style_for_toggle(
1431                            ActiveBreakpointStripMode::Condition,
1432                            has_condition,
1433                        ))
1434                        .icon_size(IconSize::Small)
1435                        .icon_color(color_for_toggle(has_condition))
1436                        .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1437                        .disabled(!supports_condition)
1438                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1439                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1440                        .tooltip(|window, cx| {
1441                            Tooltip::with_meta(
1442                                "Set Condition",
1443                                None,
1444                                "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
1445                                window,
1446                                cx,
1447                            )
1448                        }),
1449                    )
1450                    .when(!has_condition && !self.is_selected, |this| this.invisible()),
1451            )
1452            .child(
1453                div()
1454                    .map(self.add_focus_styles(
1455                        ActiveBreakpointStripMode::HitCondition,
1456                        supports_hit_condition,
1457                        window,
1458                        cx,
1459                    ))
1460                    .child(
1461                        IconButton::new(
1462                            SharedString::from(format!("{id}-hit-condition-toggle")),
1463                            IconName::ArrowDown10,
1464                        )
1465                        .style(style_for_toggle(
1466                            ActiveBreakpointStripMode::HitCondition,
1467                            has_hit_condition,
1468                        ))
1469                        .shape(ui::IconButtonShape::Square)
1470                        .icon_size(IconSize::Small)
1471                        .icon_color(color_for_toggle(has_hit_condition))
1472                        .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1473                        .disabled(!supports_hit_condition)
1474                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1475                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
1476                        .tooltip(|window, cx| {
1477                            Tooltip::with_meta(
1478                                "Set Hit Condition",
1479                                None,
1480                                "Set expression that controls how many hits of the breakpoint are ignored.",
1481                                window,
1482                                cx,
1483                            )
1484                        }),
1485                    )
1486                    .when(!has_hit_condition && !self.is_selected, |this| {
1487                        this.invisible()
1488                    }),
1489            )
1490    }
1491}