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}