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