breakpoint_list.rs

  1use std::{
  2    ops::Range,
  3    path::{Path, PathBuf},
  4    sync::Arc,
  5    time::Duration,
  6};
  7
  8use dap::ExceptionBreakpointsFilter;
  9use editor::Editor;
 10use gpui::{
 11    Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
 12    Task, UniformListScrollHandle, WeakEntity, 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    AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
 25    Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
 26    LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
 27    SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
 28    v_flex,
 29};
 30use util::ResultExt;
 31use workspace::Workspace;
 32use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
 33
 34#[derive(Clone, Copy, PartialEq)]
 35pub(crate) enum SelectedBreakpointKind {
 36    Source,
 37    Exception,
 38}
 39pub(crate) struct BreakpointList {
 40    workspace: WeakEntity<Workspace>,
 41    breakpoint_store: Entity<BreakpointStore>,
 42    worktree_store: Entity<WorktreeStore>,
 43    scrollbar_state: ScrollbarState,
 44    breakpoints: Vec<BreakpointEntry>,
 45    session: Option<Entity<Session>>,
 46    hide_scrollbar_task: Option<Task<()>>,
 47    show_scrollbar: bool,
 48    focus_handle: FocusHandle,
 49    scroll_handle: UniformListScrollHandle,
 50    selected_ix: Option<usize>,
 51}
 52
 53impl Focusable for BreakpointList {
 54    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 55        self.focus_handle.clone()
 56    }
 57}
 58
 59impl BreakpointList {
 60    pub(crate) fn new(
 61        session: Option<Entity<Session>>,
 62        workspace: WeakEntity<Workspace>,
 63        project: &Entity<Project>,
 64        cx: &mut App,
 65    ) -> Entity<Self> {
 66        let project = project.read(cx);
 67        let breakpoint_store = project.breakpoint_store();
 68        let worktree_store = project.worktree_store();
 69        let focus_handle = cx.focus_handle();
 70        let scroll_handle = UniformListScrollHandle::new();
 71        let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 72
 73        cx.new(|_| Self {
 74            breakpoint_store,
 75            worktree_store,
 76            scrollbar_state,
 77            breakpoints: Default::default(),
 78            hide_scrollbar_task: None,
 79            show_scrollbar: false,
 80            workspace,
 81            session,
 82            focus_handle,
 83            scroll_handle,
 84            selected_ix: None,
 85        })
 86    }
 87
 88    fn edit_line_breakpoint(
 89        &mut self,
 90        path: Arc<Path>,
 91        row: u32,
 92        action: BreakpointEditAction,
 93        cx: &mut Context<Self>,
 94    ) {
 95        self.breakpoint_store.update(cx, |breakpoint_store, cx| {
 96            if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
 97                breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
 98            } else {
 99                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
100            }
101        })
102    }
103
104    fn go_to_line_breakpoint(
105        &mut self,
106        path: Arc<Path>,
107        row: u32,
108        window: &mut Window,
109        cx: &mut Context<Self>,
110    ) {
111        let task = self
112            .worktree_store
113            .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
114        cx.spawn_in(window, async move |this, cx| {
115            let (worktree, relative_path) = task.await?;
116            let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
117            let item = this
118                .update_in(cx, |this, window, cx| {
119                    this.workspace.update(cx, |this, cx| {
120                        this.open_path((worktree_id, relative_path), None, true, window, cx)
121                    })
122                })??
123                .await?;
124            if let Some(editor) = item.downcast::<Editor>() {
125                editor
126                    .update_in(cx, |this, window, cx| {
127                        this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
128                    })
129                    .ok();
130            }
131            anyhow::Ok(())
132        })
133        .detach();
134    }
135
136    pub(crate) fn selection_kind(&self) -> Option<(SelectedBreakpointKind, bool)> {
137        self.selected_ix.and_then(|ix| {
138            self.breakpoints.get(ix).map(|bp| match &bp.kind {
139                BreakpointEntryKind::LineBreakpoint(bp) => (
140                    SelectedBreakpointKind::Source,
141                    bp.breakpoint.state
142                        == project::debugger::breakpoint_store::BreakpointState::Enabled,
143                ),
144                BreakpointEntryKind::ExceptionBreakpoint(bp) => {
145                    (SelectedBreakpointKind::Exception, bp.is_enabled)
146                }
147            })
148        })
149    }
150
151    fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
152        self.selected_ix = ix;
153        if let Some(ix) = ix {
154            self.scroll_handle
155                .scroll_to_item(ix, ScrollStrategy::Center);
156        }
157        cx.notify();
158    }
159
160    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
161        let ix = match self.selected_ix {
162            _ if self.breakpoints.len() == 0 => None,
163            None => Some(0),
164            Some(ix) => {
165                if ix == self.breakpoints.len() - 1 {
166                    Some(0)
167                } else {
168                    Some(ix + 1)
169                }
170            }
171        };
172        self.select_ix(ix, cx);
173    }
174
175    fn select_previous(
176        &mut self,
177        _: &menu::SelectPrevious,
178        _window: &mut Window,
179        cx: &mut Context<Self>,
180    ) {
181        let ix = match self.selected_ix {
182            _ if self.breakpoints.len() == 0 => None,
183            None => Some(self.breakpoints.len() - 1),
184            Some(ix) => {
185                if ix == 0 {
186                    Some(self.breakpoints.len() - 1)
187                } else {
188                    Some(ix - 1)
189                }
190            }
191        };
192        self.select_ix(ix, cx);
193    }
194
195    fn select_first(
196        &mut self,
197        _: &menu::SelectFirst,
198        _window: &mut Window,
199        cx: &mut Context<Self>,
200    ) {
201        let ix = if self.breakpoints.len() > 0 {
202            Some(0)
203        } else {
204            None
205        };
206        self.select_ix(ix, cx);
207    }
208
209    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
210        let ix = if self.breakpoints.len() > 0 {
211            Some(self.breakpoints.len() - 1)
212        } else {
213            None
214        };
215        self.select_ix(ix, cx);
216    }
217
218    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
219        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
220            return;
221        };
222
223        match &mut entry.kind {
224            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
225                let path = line_breakpoint.breakpoint.path.clone();
226                let row = line_breakpoint.breakpoint.row;
227                self.go_to_line_breakpoint(path, row, window, cx);
228            }
229            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
230        }
231    }
232
233    fn toggle_enable_breakpoint(
234        &mut self,
235        _: &ToggleEnableBreakpoint,
236        _window: &mut Window,
237        cx: &mut Context<Self>,
238    ) {
239        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
240            return;
241        };
242
243        match &mut entry.kind {
244            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
245                let path = line_breakpoint.breakpoint.path.clone();
246                let row = line_breakpoint.breakpoint.row;
247                self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
248            }
249            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
250                if let Some(session) = &self.session {
251                    let id = exception_breakpoint.id.clone();
252                    session.update(cx, |session, cx| {
253                        session.toggle_exception_breakpoint(&id, cx);
254                    });
255                }
256            }
257        }
258        cx.notify();
259    }
260
261    fn unset_breakpoint(
262        &mut self,
263        _: &UnsetBreakpoint,
264        _window: &mut Window,
265        cx: &mut Context<Self>,
266    ) {
267        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
268            return;
269        };
270
271        match &mut entry.kind {
272            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
273                let path = line_breakpoint.breakpoint.path.clone();
274                let row = line_breakpoint.breakpoint.row;
275                self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
276            }
277            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
278        }
279        cx.notify();
280    }
281
282    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
283        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
284        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
285            cx.background_executor()
286                .timer(SCROLLBAR_SHOW_INTERVAL)
287                .await;
288            panel
289                .update(cx, |panel, cx| {
290                    panel.show_scrollbar = false;
291                    cx.notify();
292                })
293                .log_err();
294        }))
295    }
296
297    fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
298        let selected_ix = self.selected_ix;
299        let focus_handle = self.focus_handle.clone();
300        uniform_list(
301            "breakpoint-list",
302            self.breakpoints.len(),
303            cx.processor(move |this, range: Range<usize>, window, cx| {
304                range
305                    .clone()
306                    .zip(&mut this.breakpoints[range])
307                    .map(|(ix, breakpoint)| {
308                        breakpoint
309                            .render(ix, focus_handle.clone(), window, cx)
310                            .toggle_state(Some(ix) == selected_ix)
311                            .into_any_element()
312                    })
313                    .collect()
314            }),
315        )
316        .track_scroll(self.scroll_handle.clone())
317        .flex_grow()
318    }
319
320    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
321        if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
322            return None;
323        }
324        Some(
325            div()
326                .occlude()
327                .id("breakpoint-list-vertical-scrollbar")
328                .on_mouse_move(cx.listener(|_, _, _, cx| {
329                    cx.notify();
330                    cx.stop_propagation()
331                }))
332                .on_hover(|_, _, cx| {
333                    cx.stop_propagation();
334                })
335                .on_any_mouse_down(|_, _, cx| {
336                    cx.stop_propagation();
337                })
338                .on_mouse_up(
339                    MouseButton::Left,
340                    cx.listener(|_, _, _, cx| {
341                        cx.stop_propagation();
342                    }),
343                )
344                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
345                    cx.notify();
346                }))
347                .h_full()
348                .absolute()
349                .right_1()
350                .top_1()
351                .bottom_0()
352                .w(px(12.))
353                .cursor_default()
354                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
355        )
356    }
357    pub(crate) fn render_control_strip(&self) -> AnyElement {
358        let selection_kind = self.selection_kind();
359        let focus_handle = self.focus_handle.clone();
360        let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
361            SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
362            SelectedBreakpointKind::Exception => {
363                "Exception Breakpoints cannot be removed from the breakpoint list"
364            }
365        });
366        let toggle_label = selection_kind.map(|(_, is_enabled)| {
367            if is_enabled {
368                (
369                    "Disable Breakpoint",
370                    "Disable a breakpoint without removing it from the list",
371                )
372            } else {
373                ("Enable Breakpoint", "Re-enable a breakpoint")
374            }
375        });
376
377        h_flex()
378            .gap_2()
379            .child(
380                IconButton::new(
381                    "disable-breakpoint-breakpoint-list",
382                    IconName::DebugDisabledBreakpoint,
383                )
384                .icon_size(IconSize::XSmall)
385                .when_some(toggle_label, |this, (label, meta)| {
386                    this.tooltip({
387                        let focus_handle = focus_handle.clone();
388                        move |window, cx| {
389                            Tooltip::with_meta_in(
390                                label,
391                                Some(&ToggleEnableBreakpoint),
392                                meta,
393                                &focus_handle,
394                                window,
395                                cx,
396                            )
397                        }
398                    })
399                })
400                .disabled(selection_kind.is_none())
401                .on_click({
402                    let focus_handle = focus_handle.clone();
403                    move |_, window, cx| {
404                        focus_handle.focus(window);
405                        window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
406                    }
407                }),
408            )
409            .child(
410                IconButton::new("remove-breakpoint-breakpoint-list", IconName::X)
411                    .icon_size(IconSize::XSmall)
412                    .icon_color(ui::Color::Error)
413                    .when_some(remove_breakpoint_tooltip, |this, tooltip| {
414                        this.tooltip({
415                            let focus_handle = focus_handle.clone();
416                            move |window, cx| {
417                                Tooltip::with_meta_in(
418                                    "Remove Breakpoint",
419                                    Some(&UnsetBreakpoint),
420                                    tooltip,
421                                    &focus_handle,
422                                    window,
423                                    cx,
424                                )
425                            }
426                        })
427                    })
428                    .disabled(
429                        selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
430                    )
431                    .on_click({
432                        let focus_handle = focus_handle.clone();
433                        move |_, window, cx| {
434                            focus_handle.focus(window);
435                            window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
436                        }
437                    }),
438            )
439            .mr_2()
440            .into_any_element()
441    }
442}
443
444impl Render for BreakpointList {
445    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
446        // let old_len = self.breakpoints.len();
447        let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
448        self.breakpoints.clear();
449        let weak = cx.weak_entity();
450        let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
451            let relative_worktree_path = self
452                .worktree_store
453                .read(cx)
454                .find_worktree(&path, cx)
455                .and_then(|(worktree, relative_path)| {
456                    worktree
457                        .read(cx)
458                        .is_visible()
459                        .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
460                });
461            breakpoints.sort_by_key(|breakpoint| breakpoint.row);
462            let weak = weak.clone();
463            breakpoints.into_iter().filter_map(move |breakpoint| {
464                debug_assert_eq!(&path, &breakpoint.path);
465                let file_name = breakpoint.path.file_name()?;
466
467                let dir = relative_worktree_path
468                    .clone()
469                    .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
470                    .parent()
471                    .and_then(|parent| {
472                        parent
473                            .to_str()
474                            .map(ToOwned::to_owned)
475                            .map(SharedString::from)
476                    });
477                let name = file_name
478                    .to_str()
479                    .map(ToOwned::to_owned)
480                    .map(SharedString::from)?;
481                let weak = weak.clone();
482                let line = breakpoint.row + 1;
483                Some(BreakpointEntry {
484                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
485                        name,
486                        dir,
487                        line,
488                        breakpoint,
489                    }),
490                    weak,
491                })
492            })
493        });
494        let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
495            session
496                .read(cx)
497                .exception_breakpoints()
498                .map(|(data, is_enabled)| BreakpointEntry {
499                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
500                        id: data.filter.clone(),
501                        data: data.clone(),
502                        is_enabled: *is_enabled,
503                    }),
504                    weak: weak.clone(),
505                })
506        });
507        self.breakpoints
508            .extend(breakpoints.chain(exception_breakpoints));
509        v_flex()
510            .id("breakpoint-list")
511            .key_context("BreakpointList")
512            .track_focus(&self.focus_handle)
513            .on_hover(cx.listener(|this, hovered, window, cx| {
514                if *hovered {
515                    this.show_scrollbar = true;
516                    this.hide_scrollbar_task.take();
517                    cx.notify();
518                } else if !this.focus_handle.contains_focused(window, cx) {
519                    this.hide_scrollbar(window, cx);
520                }
521            }))
522            .on_action(cx.listener(Self::select_next))
523            .on_action(cx.listener(Self::select_previous))
524            .on_action(cx.listener(Self::select_first))
525            .on_action(cx.listener(Self::select_last))
526            .on_action(cx.listener(Self::confirm))
527            .on_action(cx.listener(Self::toggle_enable_breakpoint))
528            .on_action(cx.listener(Self::unset_breakpoint))
529            .size_full()
530            .m_0p5()
531            .child(self.render_list(window, cx))
532            .children(self.render_vertical_scrollbar(cx))
533    }
534}
535#[derive(Clone, Debug)]
536struct LineBreakpoint {
537    name: SharedString,
538    dir: Option<SharedString>,
539    line: u32,
540    breakpoint: SourceBreakpoint,
541}
542
543impl LineBreakpoint {
544    fn render(
545        &mut self,
546        ix: usize,
547        focus_handle: FocusHandle,
548        weak: WeakEntity<BreakpointList>,
549    ) -> ListItem {
550        let icon_name = if self.breakpoint.state.is_enabled() {
551            IconName::DebugBreakpoint
552        } else {
553            IconName::DebugDisabledBreakpoint
554        };
555        let path = self.breakpoint.path.clone();
556        let row = self.breakpoint.row;
557        let is_enabled = self.breakpoint.state.is_enabled();
558        let indicator = div()
559            .id(SharedString::from(format!(
560                "breakpoint-ui-toggle-{:?}/{}:{}",
561                self.dir, self.name, self.line
562            )))
563            .cursor_pointer()
564            .tooltip({
565                let focus_handle = focus_handle.clone();
566                move |window, cx| {
567                    Tooltip::for_action_in(
568                        if is_enabled {
569                            "Disable Breakpoint"
570                        } else {
571                            "Enable Breakpoint"
572                        },
573                        &ToggleEnableBreakpoint,
574                        &focus_handle,
575                        window,
576                        cx,
577                    )
578                }
579            })
580            .on_click({
581                let weak = weak.clone();
582                let path = path.clone();
583                move |_, _, cx| {
584                    weak.update(cx, |breakpoint_list, cx| {
585                        breakpoint_list.edit_line_breakpoint(
586                            path.clone(),
587                            row,
588                            BreakpointEditAction::InvertState,
589                            cx,
590                        );
591                    })
592                    .ok();
593                }
594            })
595            .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
596            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
597        ListItem::new(SharedString::from(format!(
598            "breakpoint-ui-item-{:?}/{}:{}",
599            self.dir, self.name, self.line
600        )))
601        .on_click({
602            let weak = weak.clone();
603            move |_, _, cx| {
604                weak.update(cx, |breakpoint_list, cx| {
605                    breakpoint_list.select_ix(Some(ix), cx);
606                })
607                .ok();
608            }
609        })
610        .start_slot(indicator)
611        .rounded()
612        .on_secondary_mouse_down(|_, _, cx| {
613            cx.stop_propagation();
614        })
615        .child(
616            v_flex()
617                .py_1()
618                .gap_1()
619                .min_h(px(26.))
620                .justify_center()
621                .id(SharedString::from(format!(
622                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
623                    self.dir, self.name, self.line
624                )))
625                .on_click(move |_, window, cx| {
626                    weak.update(cx, |breakpoint_list, cx| {
627                        breakpoint_list.select_ix(Some(ix), cx);
628                        breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
629                    })
630                    .ok();
631                })
632                .cursor_pointer()
633                .child(
634                    h_flex()
635                        .gap_1()
636                        .child(
637                            Label::new(format!("{}:{}", self.name, self.line))
638                                .size(LabelSize::Small)
639                                .line_height_style(ui::LineHeightStyle::UiLabel),
640                        )
641                        .children(self.dir.clone().map(|dir| {
642                            Label::new(dir)
643                                .color(Color::Muted)
644                                .size(LabelSize::Small)
645                                .line_height_style(ui::LineHeightStyle::UiLabel)
646                        })),
647                ),
648        )
649    }
650}
651#[derive(Clone, Debug)]
652struct ExceptionBreakpoint {
653    id: String,
654    data: ExceptionBreakpointsFilter,
655    is_enabled: bool,
656}
657
658impl ExceptionBreakpoint {
659    fn render(
660        &mut self,
661        ix: usize,
662        focus_handle: FocusHandle,
663        list: WeakEntity<BreakpointList>,
664    ) -> ListItem {
665        let color = if self.is_enabled {
666            Color::Debugger
667        } else {
668            Color::Muted
669        };
670        let id = SharedString::from(&self.id);
671        let is_enabled = self.is_enabled;
672
673        ListItem::new(SharedString::from(format!(
674            "exception-breakpoint-ui-item-{}",
675            self.id
676        )))
677        .on_click({
678            let list = list.clone();
679            move |_, _, cx| {
680                list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
681                    .ok();
682            }
683        })
684        .rounded()
685        .on_secondary_mouse_down(|_, _, cx| {
686            cx.stop_propagation();
687        })
688        .start_slot(
689            div()
690                .id(SharedString::from(format!(
691                    "exception-breakpoint-ui-item-{}-click-handler",
692                    self.id
693                )))
694                .tooltip(move |window, cx| {
695                    Tooltip::for_action_in(
696                        if is_enabled {
697                            "Disable Exception Breakpoint"
698                        } else {
699                            "Enable Exception Breakpoint"
700                        },
701                        &ToggleEnableBreakpoint,
702                        &focus_handle,
703                        window,
704                        cx,
705                    )
706                })
707                .on_click({
708                    let list = list.clone();
709                    move |_, _, cx| {
710                        list.update(cx, |this, cx| {
711                            if let Some(session) = &this.session {
712                                session.update(cx, |this, cx| {
713                                    this.toggle_exception_breakpoint(&id, cx);
714                                });
715                                cx.notify();
716                            }
717                        })
718                        .ok();
719                    }
720                })
721                .cursor_pointer()
722                .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
723        )
724        .child(
725            v_flex()
726                .py_1()
727                .gap_1()
728                .min_h(px(26.))
729                .justify_center()
730                .id(("exception-breakpoint-label", ix))
731                .child(
732                    Label::new(self.data.label.clone())
733                        .size(LabelSize::Small)
734                        .line_height_style(ui::LineHeightStyle::UiLabel),
735                )
736                .when_some(self.data.description.clone(), |el, description| {
737                    el.tooltip(Tooltip::text(description))
738                }),
739        )
740    }
741}
742#[derive(Clone, Debug)]
743enum BreakpointEntryKind {
744    LineBreakpoint(LineBreakpoint),
745    ExceptionBreakpoint(ExceptionBreakpoint),
746}
747
748#[derive(Clone, Debug)]
749struct BreakpointEntry {
750    kind: BreakpointEntryKind,
751    weak: WeakEntity<BreakpointList>,
752}
753
754impl BreakpointEntry {
755    fn render(
756        &mut self,
757        ix: usize,
758        focus_handle: FocusHandle,
759        _: &mut Window,
760        _: &mut App,
761    ) -> ListItem {
762        match &mut self.kind {
763            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
764                line_breakpoint.render(ix, focus_handle, self.weak.clone())
765            }
766            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
767                exception_breakpoint.render(ix, focus_handle, self.weak.clone())
768            }
769        }
770    }
771}