breakpoint_list.rs

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