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