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::KeyValueStore;
  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, cx);
 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, cx);
 368            } else {
 369                handle.focus(window, cx);
 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            let kvp = KeyValueStore::global(cx);
 524            cx.background_executor()
 525                .spawn(async move { kvp.write_kvp(key, value?).await })
 526        } else {
 527            Task::ready(Result::Ok(()))
 528        }
 529    }
 530
 531    fn deserialize_exception_breakpoints(
 532        &self,
 533        adapter_name: DebugAdapterName,
 534        cx: &mut Context<Self>,
 535    ) -> anyhow::Result<()> {
 536        let Some(val) = KeyValueStore::global(cx).read_kvp(&Self::kvp_key(&adapter_name))? else {
 537            return Ok(());
 538        };
 539        let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
 540        self.dap_store
 541            .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
 542
 543        Ok(())
 544    }
 545
 546    fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 547        let selected_ix = self.selected_ix;
 548        let focus_handle = self.focus_handle.clone();
 549        let supported_breakpoint_properties = self
 550            .session
 551            .as_ref()
 552            .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
 553            .unwrap_or_else(SupportedBreakpointProperties::all);
 554        let strip_mode = self.strip_mode;
 555
 556        uniform_list(
 557            "breakpoint-list",
 558            self.breakpoints.len(),
 559            cx.processor(move |this, range: Range<usize>, _, _| {
 560                range
 561                    .clone()
 562                    .zip(&mut this.breakpoints[range])
 563                    .map(|(ix, breakpoint)| {
 564                        breakpoint
 565                            .render(
 566                                strip_mode,
 567                                supported_breakpoint_properties,
 568                                ix,
 569                                Some(ix) == selected_ix,
 570                                focus_handle.clone(),
 571                            )
 572                            .into_any_element()
 573                    })
 574                    .collect()
 575            }),
 576        )
 577        .with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
 578        .with_width_from_item(self.max_width_index)
 579        .track_scroll(&self.scroll_handle)
 580        .flex_1()
 581    }
 582
 583    pub(crate) fn render_control_strip(&self) -> AnyElement {
 584        let selection_kind = self.selection_kind();
 585        let focus_handle = self.focus_handle.clone();
 586
 587        let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
 588            SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
 589            SelectedBreakpointKind::Exception => {
 590                "Exception Breakpoints cannot be removed from the breakpoint list"
 591            }
 592            SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
 593        });
 594
 595        let toggle_label = selection_kind.map(|(_, is_enabled)| {
 596            if is_enabled {
 597                (
 598                    "Disable Breakpoint",
 599                    "Disable a breakpoint without removing it from the list",
 600                )
 601            } else {
 602                ("Enable Breakpoint", "Re-enable a breakpoint")
 603            }
 604        });
 605
 606        h_flex()
 607            .child(
 608                IconButton::new(
 609                    "disable-breakpoint-breakpoint-list",
 610                    IconName::DebugDisabledBreakpoint,
 611                )
 612                .icon_size(IconSize::Small)
 613                .when_some(toggle_label, |this, (label, meta)| {
 614                    this.tooltip({
 615                        let focus_handle = focus_handle.clone();
 616                        move |_window, cx| {
 617                            Tooltip::with_meta_in(
 618                                label,
 619                                Some(&ToggleEnableBreakpoint),
 620                                meta,
 621                                &focus_handle,
 622                                cx,
 623                            )
 624                        }
 625                    })
 626                })
 627                .disabled(selection_kind.is_none())
 628                .on_click({
 629                    let focus_handle = focus_handle.clone();
 630                    move |_, window, cx| {
 631                        focus_handle.focus(window, cx);
 632                        window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
 633                    }
 634                }),
 635            )
 636            .child(
 637                IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
 638                    .icon_size(IconSize::Small)
 639                    .when_some(remove_breakpoint_tooltip, |this, tooltip| {
 640                        this.tooltip({
 641                            let focus_handle = focus_handle.clone();
 642                            move |_window, cx| {
 643                                Tooltip::with_meta_in(
 644                                    "Remove Breakpoint",
 645                                    Some(&UnsetBreakpoint),
 646                                    tooltip,
 647                                    &focus_handle,
 648                                    cx,
 649                                )
 650                            }
 651                        })
 652                    })
 653                    .disabled(
 654                        selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
 655                    )
 656                    .on_click({
 657                        move |_, window, cx| {
 658                            focus_handle.focus(window, cx);
 659                            window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
 660                        }
 661                    }),
 662            )
 663            .into_any_element()
 664    }
 665}
 666
 667impl Render for BreakpointList {
 668    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
 669        let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
 670        self.breakpoints.clear();
 671        let path_style = self.worktree_store.read(cx).path_style();
 672        let weak = cx.weak_entity();
 673        let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
 674            let relative_worktree_path = self
 675                .worktree_store
 676                .read(cx)
 677                .find_worktree(&path, cx)
 678                .and_then(|(worktree, relative_path)| {
 679                    worktree
 680                        .read(cx)
 681                        .is_visible()
 682                        .then(|| worktree.read(cx).root_name().join(&relative_path))
 683                });
 684            breakpoints.sort_by_key(|breakpoint| breakpoint.row);
 685            let weak = weak.clone();
 686            breakpoints.into_iter().filter_map(move |breakpoint| {
 687                debug_assert_eq!(&path, &breakpoint.path);
 688                let file_name = breakpoint.path.file_name()?;
 689                let breakpoint_path = RelPath::new(&breakpoint.path, path_style).ok();
 690
 691                let dir = relative_worktree_path
 692                    .as_deref()
 693                    .or(breakpoint_path.as_deref())?
 694                    .parent()
 695                    .map(|parent| SharedString::from(parent.display(path_style).to_string()));
 696                let name = file_name
 697                    .to_str()
 698                    .map(ToOwned::to_owned)
 699                    .map(SharedString::from)?;
 700                let weak = weak.clone();
 701                let line = breakpoint.row + 1;
 702                Some(BreakpointEntry {
 703                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
 704                        name,
 705                        dir,
 706                        line,
 707                        breakpoint,
 708                    }),
 709                    weak,
 710                })
 711            })
 712        });
 713        let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 714            session
 715                .read(cx)
 716                .exception_breakpoints()
 717                .map(|(data, is_enabled)| BreakpointEntry {
 718                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
 719                        id: data.filter.clone(),
 720                        data: data.clone(),
 721                        is_enabled: *is_enabled,
 722                    }),
 723                    weak: weak.clone(),
 724                })
 725        });
 726        let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
 727            session
 728                .read(cx)
 729                .data_breakpoints()
 730                .map(|state| BreakpointEntry {
 731                    kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
 732                    weak: weak.clone(),
 733                })
 734        });
 735        self.breakpoints.extend(
 736            breakpoints
 737                .chain(data_breakpoints)
 738                .chain(exception_breakpoints),
 739        );
 740
 741        let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
 742
 743        self.max_width_index = self
 744            .breakpoints
 745            .iter()
 746            .map(|entry| match &entry.kind {
 747                BreakpointEntryKind::LineBreakpoint(line_bp) => {
 748                    let name_and_line = format!("{}:{}", line_bp.name, line_bp.line);
 749                    let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0);
 750                    (name_and_line.len() + dir_len) as f32 * text_pixels
 751                }
 752                BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => {
 753                    exc_bp.data.label.len() as f32 * text_pixels
 754                }
 755                BreakpointEntryKind::DataBreakpoint(data_bp) => {
 756                    data_bp.0.context.human_readable_label().len() as f32 * text_pixels
 757                }
 758            })
 759            .position_max_by(|left, right| left.total_cmp(right));
 760
 761        v_flex()
 762            .id("breakpoint-list")
 763            .key_context("BreakpointList")
 764            .track_focus(&self.focus_handle)
 765            .on_action(cx.listener(Self::select_next))
 766            .on_action(cx.listener(Self::select_previous))
 767            .on_action(cx.listener(Self::select_first))
 768            .on_action(cx.listener(Self::select_last))
 769            .on_action(cx.listener(Self::dismiss))
 770            .on_action(cx.listener(Self::confirm))
 771            .on_action(cx.listener(Self::toggle_enable_breakpoint))
 772            .on_action(cx.listener(Self::unset_breakpoint))
 773            .on_action(cx.listener(Self::next_breakpoint_property))
 774            .on_action(cx.listener(Self::previous_breakpoint_property))
 775            .size_full()
 776            .pt_1()
 777            .child(self.render_list(cx))
 778            .custom_scrollbars(
 779                ui::Scrollbars::new(ScrollAxes::Both)
 780                    .tracked_scroll_handle(&self.scroll_handle)
 781                    .with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
 782                    .tracked_entity(cx.entity_id()),
 783                window,
 784                cx,
 785            )
 786            .when_some(self.strip_mode, |this, _| {
 787                this.child(Divider::horizontal().color(DividerColor::Border))
 788                    .child(
 789                        h_flex()
 790                            .p_1()
 791                            .rounded_sm()
 792                            .bg(cx.theme().colors().editor_background)
 793                            .border_1()
 794                            .when(
 795                                self.input.focus_handle(cx).contains_focused(window, cx),
 796                                |this| {
 797                                    let colors = cx.theme().colors();
 798
 799                                    let border_color = if self.input.read(cx).read_only(cx) {
 800                                        colors.border_disabled
 801                                    } else {
 802                                        colors.border_transparent
 803                                    };
 804
 805                                    this.border_color(border_color)
 806                                },
 807                            )
 808                            .child(self.input.clone()),
 809                    )
 810            })
 811    }
 812}
 813
 814#[derive(Clone, Debug)]
 815struct LineBreakpoint {
 816    name: SharedString,
 817    dir: Option<SharedString>,
 818    line: u32,
 819    breakpoint: SourceBreakpoint,
 820}
 821
 822impl LineBreakpoint {
 823    fn render(
 824        &mut self,
 825        props: SupportedBreakpointProperties,
 826        strip_mode: Option<ActiveBreakpointStripMode>,
 827        ix: usize,
 828        is_selected: bool,
 829        focus_handle: FocusHandle,
 830        weak: WeakEntity<BreakpointList>,
 831    ) -> ListItem {
 832        let icon_name = if self.breakpoint.state.is_enabled() {
 833            IconName::DebugBreakpoint
 834        } else {
 835            IconName::DebugDisabledBreakpoint
 836        };
 837        let path = self.breakpoint.path.clone();
 838        let row = self.breakpoint.row;
 839        let is_enabled = self.breakpoint.state.is_enabled();
 840
 841        let indicator = div()
 842            .id(SharedString::from(format!(
 843                "breakpoint-ui-toggle-{:?}/{}:{}",
 844                self.dir, self.name, self.line
 845            )))
 846            .child(
 847                Icon::new(icon_name)
 848                    .color(Color::Debugger)
 849                    .size(IconSize::XSmall),
 850            )
 851            .tooltip({
 852                let focus_handle = focus_handle.clone();
 853                move |_window, cx| {
 854                    Tooltip::for_action_in(
 855                        if is_enabled {
 856                            "Disable Breakpoint"
 857                        } else {
 858                            "Enable Breakpoint"
 859                        },
 860                        &ToggleEnableBreakpoint,
 861                        &focus_handle,
 862                        cx,
 863                    )
 864                }
 865            })
 866            .on_click({
 867                let weak = weak.clone();
 868                let path = path.clone();
 869                move |_, _, cx| {
 870                    weak.update(cx, |breakpoint_list, cx| {
 871                        breakpoint_list.edit_line_breakpoint(
 872                            path.clone(),
 873                            row,
 874                            BreakpointEditAction::InvertState,
 875                            cx,
 876                        );
 877                    })
 878                    .ok();
 879                }
 880            })
 881            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
 882
 883        ListItem::new(SharedString::from(format!(
 884            "breakpoint-ui-item-{:?}/{}:{}",
 885            self.dir, self.name, self.line
 886        )))
 887        .toggle_state(is_selected)
 888        .inset(true)
 889        .on_click({
 890            let weak = weak.clone();
 891            move |_, window, cx| {
 892                weak.update(cx, |breakpoint_list, cx| {
 893                    breakpoint_list.select_ix(Some(ix), window, cx);
 894                })
 895                .ok();
 896            }
 897        })
 898        .on_secondary_mouse_down(|_, _, cx| {
 899            cx.stop_propagation();
 900        })
 901        .start_slot(indicator)
 902        .child(
 903            h_flex()
 904                .id(SharedString::from(format!(
 905                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
 906                    self.dir, self.name, self.line
 907                )))
 908                .w_full()
 909                .gap_1()
 910                .min_h(rems_from_px(26.))
 911                .justify_between()
 912                .on_click({
 913                    let weak = weak.clone();
 914                    move |_, window, cx| {
 915                        weak.update(cx, |breakpoint_list, cx| {
 916                            breakpoint_list.select_ix(Some(ix), window, cx);
 917                            breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
 918                        })
 919                        .ok();
 920                    }
 921                })
 922                .child(
 923                    h_flex()
 924                        .id("label-container")
 925                        .gap_0p5()
 926                        .child(
 927                            Label::new(format!("{}:{}", self.name, self.line))
 928                                .size(LabelSize::Small)
 929                                .line_height_style(ui::LineHeightStyle::UiLabel),
 930                        )
 931                        .children(self.dir.as_ref().and_then(|dir| {
 932                            let path_without_root = Path::new(dir.as_ref())
 933                                .components()
 934                                .skip(1)
 935                                .collect::<PathBuf>();
 936                            path_without_root.components().next()?;
 937                            Some(
 938                                Label::new(path_without_root.to_string_lossy().into_owned())
 939                                    .color(Color::Muted)
 940                                    .size(LabelSize::Small)
 941                                    .line_height_style(ui::LineHeightStyle::UiLabel)
 942                                    .truncate(),
 943                            )
 944                        }))
 945                        .when_some(self.dir.as_ref(), |this, parent_dir| {
 946                            this.tooltip(Tooltip::text(format!(
 947                                "Worktree parent path: {parent_dir}"
 948                            )))
 949                        }),
 950                )
 951                .child(BreakpointOptionsStrip {
 952                    props,
 953                    breakpoint: BreakpointEntry {
 954                        kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
 955                        weak,
 956                    },
 957                    is_selected,
 958                    focus_handle,
 959                    strip_mode,
 960                    index: ix,
 961                }),
 962        )
 963    }
 964}
 965
 966#[derive(Clone, Debug)]
 967struct ExceptionBreakpoint {
 968    id: String,
 969    data: ExceptionBreakpointsFilter,
 970    is_enabled: bool,
 971}
 972
 973#[derive(Clone, Debug)]
 974struct DataBreakpoint(project::debugger::session::DataBreakpointState);
 975
 976impl DataBreakpoint {
 977    fn render(
 978        &self,
 979        props: SupportedBreakpointProperties,
 980        strip_mode: Option<ActiveBreakpointStripMode>,
 981        ix: usize,
 982        is_selected: bool,
 983        focus_handle: FocusHandle,
 984        list: WeakEntity<BreakpointList>,
 985    ) -> ListItem {
 986        let color = if self.0.is_enabled {
 987            Color::Debugger
 988        } else {
 989            Color::Muted
 990        };
 991        let is_enabled = self.0.is_enabled;
 992        let id = self.0.dap.data_id.clone();
 993
 994        ListItem::new(SharedString::from(format!(
 995            "data-breakpoint-ui-item-{}",
 996            self.0.dap.data_id
 997        )))
 998        .toggle_state(is_selected)
 999        .inset(true)
1000        .start_slot(
1001            div()
1002                .id(SharedString::from(format!(
1003                    "data-breakpoint-ui-item-{}-click-handler",
1004                    self.0.dap.data_id
1005                )))
1006                .child(
1007                    Icon::new(IconName::Binary)
1008                        .color(color)
1009                        .size(IconSize::Small),
1010                )
1011                .tooltip({
1012                    let focus_handle = focus_handle.clone();
1013                    move |_window, cx| {
1014                        Tooltip::for_action_in(
1015                            if is_enabled {
1016                                "Disable Data Breakpoint"
1017                            } else {
1018                                "Enable Data Breakpoint"
1019                            },
1020                            &ToggleEnableBreakpoint,
1021                            &focus_handle,
1022                            cx,
1023                        )
1024                    }
1025                })
1026                .on_click({
1027                    let list = list.clone();
1028                    move |_, _, cx| {
1029                        list.update(cx, |this, cx| {
1030                            this.toggle_data_breakpoint(&id, cx);
1031                        })
1032                        .ok();
1033                    }
1034                }),
1035        )
1036        .child(
1037            h_flex()
1038                .w_full()
1039                .gap_1()
1040                .min_h(rems_from_px(26.))
1041                .justify_between()
1042                .child(
1043                    v_flex()
1044                        .py_1()
1045                        .gap_1()
1046                        .justify_center()
1047                        .id(("data-breakpoint-label", ix))
1048                        .child(
1049                            Label::new(self.0.context.human_readable_label())
1050                                .size(LabelSize::Small)
1051                                .line_height_style(ui::LineHeightStyle::UiLabel),
1052                        ),
1053                )
1054                .child(BreakpointOptionsStrip {
1055                    props,
1056                    breakpoint: BreakpointEntry {
1057                        kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1058                        weak: list,
1059                    },
1060                    is_selected,
1061                    focus_handle,
1062                    strip_mode,
1063                    index: ix,
1064                }),
1065        )
1066    }
1067}
1068
1069impl ExceptionBreakpoint {
1070    fn render(
1071        &mut self,
1072        props: SupportedBreakpointProperties,
1073        strip_mode: Option<ActiveBreakpointStripMode>,
1074        ix: usize,
1075        is_selected: bool,
1076        focus_handle: FocusHandle,
1077        list: WeakEntity<BreakpointList>,
1078    ) -> ListItem {
1079        let color = if self.is_enabled {
1080            Color::Debugger
1081        } else {
1082            Color::Muted
1083        };
1084        let id = SharedString::from(&self.id);
1085        let is_enabled = self.is_enabled;
1086        let weak = list.clone();
1087
1088        ListItem::new(SharedString::from(format!(
1089            "exception-breakpoint-ui-item-{}",
1090            self.id
1091        )))
1092        .toggle_state(is_selected)
1093        .inset(true)
1094        .on_click({
1095            let list = list.clone();
1096            move |_, window, cx| {
1097                list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1098                    .ok();
1099            }
1100        })
1101        .on_secondary_mouse_down(|_, _, cx| {
1102            cx.stop_propagation();
1103        })
1104        .start_slot(
1105            div()
1106                .id(SharedString::from(format!(
1107                    "exception-breakpoint-ui-item-{}-click-handler",
1108                    self.id
1109                )))
1110                .child(
1111                    Icon::new(IconName::Flame)
1112                        .color(color)
1113                        .size(IconSize::Small),
1114                )
1115                .tooltip({
1116                    let focus_handle = focus_handle.clone();
1117                    move |_window, cx| {
1118                        Tooltip::for_action_in(
1119                            if is_enabled {
1120                                "Disable Exception Breakpoint"
1121                            } else {
1122                                "Enable Exception Breakpoint"
1123                            },
1124                            &ToggleEnableBreakpoint,
1125                            &focus_handle,
1126                            cx,
1127                        )
1128                    }
1129                })
1130                .on_click({
1131                    move |_, _, cx| {
1132                        list.update(cx, |this, cx| {
1133                            this.toggle_exception_breakpoint(&id, cx);
1134                        })
1135                        .ok();
1136                    }
1137                }),
1138        )
1139        .child(
1140            h_flex()
1141                .w_full()
1142                .gap_1()
1143                .min_h(rems_from_px(26.))
1144                .justify_between()
1145                .child(
1146                    v_flex()
1147                        .py_1()
1148                        .gap_1()
1149                        .justify_center()
1150                        .id(("exception-breakpoint-label", ix))
1151                        .child(
1152                            Label::new(self.data.label.clone())
1153                                .size(LabelSize::Small)
1154                                .line_height_style(ui::LineHeightStyle::UiLabel),
1155                        )
1156                        .when_some(self.data.description.clone(), |el, description| {
1157                            el.tooltip(Tooltip::text(description))
1158                        }),
1159                )
1160                .child(BreakpointOptionsStrip {
1161                    props,
1162                    breakpoint: BreakpointEntry {
1163                        kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1164                        weak,
1165                    },
1166                    is_selected,
1167                    focus_handle,
1168                    strip_mode,
1169                    index: ix,
1170                }),
1171        )
1172    }
1173}
1174#[derive(Clone, Debug)]
1175enum BreakpointEntryKind {
1176    LineBreakpoint(LineBreakpoint),
1177    ExceptionBreakpoint(ExceptionBreakpoint),
1178    DataBreakpoint(DataBreakpoint),
1179}
1180
1181#[derive(Clone, Debug)]
1182struct BreakpointEntry {
1183    kind: BreakpointEntryKind,
1184    weak: WeakEntity<BreakpointList>,
1185}
1186
1187impl BreakpointEntry {
1188    fn render(
1189        &mut self,
1190        strip_mode: Option<ActiveBreakpointStripMode>,
1191        props: SupportedBreakpointProperties,
1192        ix: usize,
1193        is_selected: bool,
1194        focus_handle: FocusHandle,
1195    ) -> ListItem {
1196        match &mut self.kind {
1197            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1198                props,
1199                strip_mode,
1200                ix,
1201                is_selected,
1202                focus_handle,
1203                self.weak.clone(),
1204            ),
1205            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1206                .render(
1207                    props.for_exception_breakpoints(),
1208                    strip_mode,
1209                    ix,
1210                    is_selected,
1211                    focus_handle,
1212                    self.weak.clone(),
1213                ),
1214            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1215                props.for_data_breakpoints(),
1216                strip_mode,
1217                ix,
1218                is_selected,
1219                focus_handle,
1220                self.weak.clone(),
1221            ),
1222        }
1223    }
1224
1225    fn id(&self) -> SharedString {
1226        match &self.kind {
1227            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1228                "source-breakpoint-control-strip-{:?}:{}",
1229                line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1230            )
1231            .into(),
1232            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1233                "exception-breakpoint-control-strip--{}",
1234                exception_breakpoint.id
1235            )
1236            .into(),
1237            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1238                "data-breakpoint-control-strip--{}",
1239                data_breakpoint.0.dap.data_id
1240            )
1241            .into(),
1242        }
1243    }
1244
1245    fn has_log(&self) -> bool {
1246        match &self.kind {
1247            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1248                line_breakpoint.breakpoint.message.is_some()
1249            }
1250            _ => false,
1251        }
1252    }
1253
1254    fn has_condition(&self) -> bool {
1255        match &self.kind {
1256            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1257                line_breakpoint.breakpoint.condition.is_some()
1258            }
1259            // We don't support conditions on exception/data breakpoints
1260            _ => false,
1261        }
1262    }
1263
1264    fn has_hit_condition(&self) -> bool {
1265        match &self.kind {
1266            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1267                line_breakpoint.breakpoint.hit_condition.is_some()
1268            }
1269            _ => false,
1270        }
1271    }
1272}
1273
1274bitflags::bitflags! {
1275    #[derive(Clone, Copy)]
1276    pub struct SupportedBreakpointProperties: u32 {
1277        const LOG = 1 << 0;
1278        const CONDITION = 1 << 1;
1279        const HIT_CONDITION = 1 << 2;
1280        // Conditions for exceptions can be set only when exception filters are supported.
1281        const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1282    }
1283}
1284
1285impl From<&Capabilities> for SupportedBreakpointProperties {
1286    fn from(caps: &Capabilities) -> Self {
1287        let mut this = Self::empty();
1288        for (prop, offset) in [
1289            (caps.supports_log_points, Self::LOG),
1290            (caps.supports_conditional_breakpoints, Self::CONDITION),
1291            (
1292                caps.supports_hit_conditional_breakpoints,
1293                Self::HIT_CONDITION,
1294            ),
1295            (
1296                caps.supports_exception_options,
1297                Self::EXCEPTION_FILTER_OPTIONS,
1298            ),
1299        ] {
1300            if prop.unwrap_or_default() {
1301                this.insert(offset);
1302            }
1303        }
1304        this
1305    }
1306}
1307
1308impl SupportedBreakpointProperties {
1309    fn for_exception_breakpoints(self) -> Self {
1310        // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1311        Self::empty()
1312    }
1313    fn for_data_breakpoints(self) -> Self {
1314        // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1315        Self::empty()
1316    }
1317}
1318#[derive(IntoElement)]
1319struct BreakpointOptionsStrip {
1320    props: SupportedBreakpointProperties,
1321    breakpoint: BreakpointEntry,
1322    is_selected: bool,
1323    focus_handle: FocusHandle,
1324    strip_mode: Option<ActiveBreakpointStripMode>,
1325    index: usize,
1326}
1327
1328impl BreakpointOptionsStrip {
1329    fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1330        self.is_selected && self.strip_mode == Some(expected_mode)
1331    }
1332
1333    fn on_click_callback(
1334        &self,
1335        mode: ActiveBreakpointStripMode,
1336    ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1337        let list = self.breakpoint.weak.clone();
1338        let ix = self.index;
1339        move |_, window, cx| {
1340            list.update(cx, |this, cx| {
1341                if this.strip_mode != Some(mode) {
1342                    this.set_active_breakpoint_property(mode, window, cx);
1343                } else if this.selected_ix == Some(ix) {
1344                    this.strip_mode.take();
1345                } else {
1346                    cx.propagate();
1347                }
1348            })
1349            .ok();
1350        }
1351    }
1352
1353    fn add_focus_styles(
1354        &self,
1355        kind: ActiveBreakpointStripMode,
1356        available: bool,
1357        window: &Window,
1358        cx: &App,
1359    ) -> impl Fn(Div) -> Div {
1360        move |this: Div| {
1361            // Avoid layout shifts in case there's no colored border
1362            let this = this.border_1().rounded_sm();
1363            let color = cx.theme().colors();
1364
1365            if self.is_selected && self.strip_mode == Some(kind) {
1366                if self.focus_handle.is_focused(window) {
1367                    this.bg(color.editor_background)
1368                        .border_color(color.border_focused)
1369                } else {
1370                    this.border_color(color.border)
1371                }
1372            } else if !available {
1373                this.border_color(color.border_transparent)
1374            } else {
1375                this
1376            }
1377        }
1378    }
1379}
1380
1381impl RenderOnce for BreakpointOptionsStrip {
1382    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1383        let id = self.breakpoint.id();
1384        let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1385        let supports_condition = self
1386            .props
1387            .contains(SupportedBreakpointProperties::CONDITION);
1388        let supports_hit_condition = self
1389            .props
1390            .contains(SupportedBreakpointProperties::HIT_CONDITION);
1391        let has_logs = self.breakpoint.has_log();
1392        let has_condition = self.breakpoint.has_condition();
1393        let has_hit_condition = self.breakpoint.has_hit_condition();
1394        let style_for_toggle = |mode, is_enabled| {
1395            if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1396                ui::ButtonStyle::Filled
1397            } else {
1398                ui::ButtonStyle::Subtle
1399            }
1400        };
1401        let color_for_toggle = |is_enabled| {
1402            if is_enabled {
1403                Color::Default
1404            } else {
1405                Color::Muted
1406            }
1407        };
1408
1409        h_flex()
1410            .gap_px()
1411            .justify_end()
1412            .when(has_logs || self.is_selected, |this| {
1413                this.child(
1414                    div()
1415                    .map(self.add_focus_styles(
1416                        ActiveBreakpointStripMode::Log,
1417                        supports_logs,
1418                        window,
1419                        cx,
1420                    ))
1421                    .child(
1422                        IconButton::new(
1423                            SharedString::from(format!("{id}-log-toggle")),
1424                            IconName::Notepad,
1425                        )
1426                        .shape(ui::IconButtonShape::Square)
1427                        .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1428                        .icon_size(IconSize::Small)
1429                        .icon_color(color_for_toggle(has_logs))
1430                        .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
1431                        .disabled(!supports_logs)
1432                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1433                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
1434                        .tooltip(|_window, cx|  {
1435                            Tooltip::with_meta(
1436                                "Set Log Message",
1437                                None,
1438                                "Set log message to display (instead of stopping) when a breakpoint is hit.",
1439                                cx,
1440                            )
1441                        }),
1442                    )
1443                )
1444            })
1445            .when(has_condition || self.is_selected, |this| {
1446                this.child(
1447                    div()
1448                        .map(self.add_focus_styles(
1449                            ActiveBreakpointStripMode::Condition,
1450                            supports_condition,
1451                            window,
1452                            cx,
1453                        ))
1454                        .child(
1455                            IconButton::new(
1456                                SharedString::from(format!("{id}-condition-toggle")),
1457                                IconName::SplitAlt,
1458                            )
1459                            .shape(ui::IconButtonShape::Square)
1460                            .style(style_for_toggle(
1461                                ActiveBreakpointStripMode::Condition,
1462                                has_condition,
1463                            ))
1464                            .icon_size(IconSize::Small)
1465                            .icon_color(color_for_toggle(has_condition))
1466                            .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1467                            .disabled(!supports_condition)
1468                            .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1469                            .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1470                            .tooltip(|_window, cx|  {
1471                                Tooltip::with_meta(
1472                                    "Set Condition",
1473                                    None,
1474                                    "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
1475                                    cx,
1476                                )
1477                            }),
1478                        )
1479                )
1480            })
1481            .when(has_hit_condition || self.is_selected, |this| {
1482                this.child(div()
1483                    .map(self.add_focus_styles(
1484                        ActiveBreakpointStripMode::HitCondition,
1485                        supports_hit_condition,
1486                        window,
1487                        cx,
1488                    ))
1489                    .child(
1490                        IconButton::new(
1491                            SharedString::from(format!("{id}-hit-condition-toggle")),
1492                            IconName::ArrowDown10,
1493                        )
1494                        .style(style_for_toggle(
1495                            ActiveBreakpointStripMode::HitCondition,
1496                            has_hit_condition,
1497                        ))
1498                        .shape(ui::IconButtonShape::Square)
1499                        .icon_size(IconSize::Small)
1500                        .icon_color(color_for_toggle(has_hit_condition))
1501                        .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1502                        .disabled(!supports_hit_condition)
1503                        .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1504                        .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
1505                        .tooltip(|_window, cx|  {
1506                            Tooltip::with_meta(
1507                                "Set Hit Condition",
1508                                None,
1509                                "Set expression that controls how many hits of the breakpoint are ignored.",
1510                                cx,
1511                            )
1512                        }),
1513                    ))
1514
1515            })
1516    }
1517}