breakpoint_list.rs

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