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