breakpoint_list.rs

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