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