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