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