breakpoint_list.rs

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