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