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