breakpoint_list.rs

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