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    AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
 12    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    App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
 25    IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
 26    ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
 27    Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
 28};
 29use util::ResultExt;
 30use workspace::Workspace;
 31use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
 32
 33pub(crate) struct BreakpointList {
 34    workspace: WeakEntity<Workspace>,
 35    breakpoint_store: Entity<BreakpointStore>,
 36    worktree_store: Entity<WorktreeStore>,
 37    scrollbar_state: ScrollbarState,
 38    breakpoints: Vec<BreakpointEntry>,
 39    session: Option<Entity<Session>>,
 40    hide_scrollbar_task: Option<Task<()>>,
 41    show_scrollbar: bool,
 42    focus_handle: FocusHandle,
 43    scroll_handle: UniformListScrollHandle,
 44    selected_ix: Option<usize>,
 45}
 46
 47impl Focusable for BreakpointList {
 48    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 49        self.focus_handle.clone()
 50    }
 51}
 52
 53impl BreakpointList {
 54    pub(crate) fn new(
 55        session: Option<Entity<Session>>,
 56        workspace: WeakEntity<Workspace>,
 57        project: &Entity<Project>,
 58        cx: &mut App,
 59    ) -> Entity<Self> {
 60        let project = project.read(cx);
 61        let breakpoint_store = project.breakpoint_store();
 62        let worktree_store = project.worktree_store();
 63        let focus_handle = cx.focus_handle();
 64        let scroll_handle = UniformListScrollHandle::new();
 65        let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 66
 67        cx.new(|_| Self {
 68            breakpoint_store,
 69            worktree_store,
 70            scrollbar_state,
 71            breakpoints: Default::default(),
 72            hide_scrollbar_task: None,
 73            show_scrollbar: false,
 74            workspace,
 75            session,
 76            focus_handle,
 77            scroll_handle,
 78            selected_ix: None,
 79        })
 80    }
 81
 82    fn edit_line_breakpoint(
 83        &mut self,
 84        path: Arc<Path>,
 85        row: u32,
 86        action: BreakpointEditAction,
 87        cx: &mut Context<Self>,
 88    ) {
 89        self.breakpoint_store.update(cx, |breakpoint_store, cx| {
 90            if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
 91                breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
 92            } else {
 93                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
 94            }
 95        })
 96    }
 97
 98    fn go_to_line_breakpoint(
 99        &mut self,
100        path: Arc<Path>,
101        row: u32,
102        window: &mut Window,
103        cx: &mut Context<Self>,
104    ) {
105        let task = self
106            .worktree_store
107            .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
108        cx.spawn_in(window, async move |this, cx| {
109            let (worktree, relative_path) = task.await?;
110            let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
111            let item = this
112                .update_in(cx, |this, window, cx| {
113                    this.workspace.update(cx, |this, cx| {
114                        this.open_path((worktree_id, relative_path), None, true, window, cx)
115                    })
116                })??
117                .await?;
118            if let Some(editor) = item.downcast::<Editor>() {
119                editor
120                    .update_in(cx, |this, window, cx| {
121                        this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
122                    })
123                    .ok();
124            }
125            anyhow::Ok(())
126        })
127        .detach();
128    }
129
130    fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
131        self.selected_ix = ix;
132        if let Some(ix) = ix {
133            self.scroll_handle
134                .scroll_to_item(ix, ScrollStrategy::Center);
135        }
136        cx.notify();
137    }
138
139    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
140        let ix = match self.selected_ix {
141            _ if self.breakpoints.len() == 0 => None,
142            None => Some(0),
143            Some(ix) => {
144                if ix == self.breakpoints.len() - 1 {
145                    Some(0)
146                } else {
147                    Some(ix + 1)
148                }
149            }
150        };
151        self.select_ix(ix, cx);
152    }
153
154    fn select_previous(
155        &mut self,
156        _: &menu::SelectPrevious,
157        _window: &mut Window,
158        cx: &mut Context<Self>,
159    ) {
160        let ix = match self.selected_ix {
161            _ if self.breakpoints.len() == 0 => None,
162            None => Some(self.breakpoints.len() - 1),
163            Some(ix) => {
164                if ix == 0 {
165                    Some(self.breakpoints.len() - 1)
166                } else {
167                    Some(ix - 1)
168                }
169            }
170        };
171        self.select_ix(ix, cx);
172    }
173
174    fn select_first(
175        &mut self,
176        _: &menu::SelectFirst,
177        _window: &mut Window,
178        cx: &mut Context<Self>,
179    ) {
180        let ix = if self.breakpoints.len() > 0 {
181            Some(0)
182        } else {
183            None
184        };
185        self.select_ix(ix, cx);
186    }
187
188    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
189        let ix = if self.breakpoints.len() > 0 {
190            Some(self.breakpoints.len() - 1)
191        } else {
192            None
193        };
194        self.select_ix(ix, cx);
195    }
196
197    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
198        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
199            return;
200        };
201
202        match &mut entry.kind {
203            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
204                let path = line_breakpoint.breakpoint.path.clone();
205                let row = line_breakpoint.breakpoint.row;
206                self.go_to_line_breakpoint(path, row, window, cx);
207            }
208            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
209        }
210    }
211
212    fn toggle_enable_breakpoint(
213        &mut self,
214        _: &ToggleEnableBreakpoint,
215        _window: &mut Window,
216        cx: &mut Context<Self>,
217    ) {
218        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
219            return;
220        };
221
222        match &mut entry.kind {
223            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
224                let path = line_breakpoint.breakpoint.path.clone();
225                let row = line_breakpoint.breakpoint.row;
226                self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
227            }
228            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
229                if let Some(session) = &self.session {
230                    let id = exception_breakpoint.id.clone();
231                    session.update(cx, |session, cx| {
232                        session.toggle_exception_breakpoint(&id, cx);
233                    });
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            "breakpoint-list",
281            self.breakpoints.len(),
282            cx.processor(move |this, range: Range<usize>, window, cx| {
283                range
284                    .clone()
285                    .zip(&mut this.breakpoints[range])
286                    .map(|(ix, breakpoint)| {
287                        breakpoint
288                            .render(ix, focus_handle.clone(), window, cx)
289                            .toggle_state(Some(ix) == selected_ix)
290                            .into_any_element()
291                    })
292                    .collect()
293            }),
294        )
295        .track_scroll(self.scroll_handle.clone())
296        .flex_grow()
297    }
298
299    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
300        if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
301            return None;
302        }
303        Some(
304            div()
305                .occlude()
306                .id("breakpoint-list-vertical-scrollbar")
307                .on_mouse_move(cx.listener(|_, _, _, cx| {
308                    cx.notify();
309                    cx.stop_propagation()
310                }))
311                .on_hover(|_, _, cx| {
312                    cx.stop_propagation();
313                })
314                .on_any_mouse_down(|_, _, cx| {
315                    cx.stop_propagation();
316                })
317                .on_mouse_up(
318                    MouseButton::Left,
319                    cx.listener(|_, _, _, cx| {
320                        cx.stop_propagation();
321                    }),
322                )
323                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
324                    cx.notify();
325                }))
326                .h_full()
327                .absolute()
328                .right_1()
329                .top_1()
330                .bottom_0()
331                .w(px(12.))
332                .cursor_default()
333                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
334        )
335    }
336}
337impl Render for BreakpointList {
338    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
339        // let old_len = self.breakpoints.len();
340        let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
341        self.breakpoints.clear();
342        let weak = cx.weak_entity();
343        let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
344            let relative_worktree_path = self
345                .worktree_store
346                .read(cx)
347                .find_worktree(&path, cx)
348                .and_then(|(worktree, relative_path)| {
349                    worktree
350                        .read(cx)
351                        .is_visible()
352                        .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
353                });
354            breakpoints.sort_by_key(|breakpoint| breakpoint.row);
355            let weak = weak.clone();
356            breakpoints.into_iter().filter_map(move |breakpoint| {
357                debug_assert_eq!(&path, &breakpoint.path);
358                let file_name = breakpoint.path.file_name()?;
359
360                let dir = relative_worktree_path
361                    .clone()
362                    .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
363                    .parent()
364                    .and_then(|parent| {
365                        parent
366                            .to_str()
367                            .map(ToOwned::to_owned)
368                            .map(SharedString::from)
369                    });
370                let name = file_name
371                    .to_str()
372                    .map(ToOwned::to_owned)
373                    .map(SharedString::from)?;
374                let weak = weak.clone();
375                let line = breakpoint.row + 1;
376                Some(BreakpointEntry {
377                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
378                        name,
379                        dir,
380                        line,
381                        breakpoint,
382                    }),
383                    weak,
384                })
385            })
386        });
387        let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
388            session
389                .read(cx)
390                .exception_breakpoints()
391                .map(|(data, is_enabled)| BreakpointEntry {
392                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
393                        id: data.filter.clone(),
394                        data: data.clone(),
395                        is_enabled: *is_enabled,
396                    }),
397                    weak: weak.clone(),
398                })
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            h_flex()
510                .child(
511                    IconButton::new(
512                        SharedString::from(format!(
513                            "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
514                            self.dir, self.name, self.line
515                        )),
516                        IconName::Close,
517                    )
518                    .on_click({
519                        let weak = weak.clone();
520                        let path = path.clone();
521                        move |_, _, cx| {
522                            weak.update(cx, |breakpoint_list, cx| {
523                                breakpoint_list.edit_line_breakpoint(
524                                    path.clone(),
525                                    row,
526                                    BreakpointEditAction::Toggle,
527                                    cx,
528                                );
529                            })
530                            .ok();
531                        }
532                    })
533                    .tooltip(move |window, cx| {
534                        Tooltip::for_action_in(
535                            "Unset Breakpoint",
536                            &UnsetBreakpoint,
537                            &focus_handle,
538                            window,
539                            cx,
540                        )
541                    })
542                    .icon_size(ui::IconSize::XSmall),
543                )
544                .right_4(),
545        )
546        .child(
547            v_flex()
548                .py_1()
549                .gap_1()
550                .min_h(px(26.))
551                .justify_center()
552                .id(SharedString::from(format!(
553                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
554                    self.dir, self.name, self.line
555                )))
556                .on_click(move |_, window, cx| {
557                    weak.update(cx, |breakpoint_list, cx| {
558                        breakpoint_list.select_ix(Some(ix), cx);
559                        breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
560                    })
561                    .ok();
562                })
563                .cursor_pointer()
564                .child(
565                    h_flex()
566                        .gap_1()
567                        .child(
568                            Label::new(format!("{}:{}", self.name, self.line))
569                                .size(LabelSize::Small)
570                                .line_height_style(ui::LineHeightStyle::UiLabel),
571                        )
572                        .children(self.dir.clone().map(|dir| {
573                            Label::new(dir)
574                                .color(Color::Muted)
575                                .size(LabelSize::Small)
576                                .line_height_style(ui::LineHeightStyle::UiLabel)
577                        })),
578                ),
579        )
580    }
581}
582#[derive(Clone, Debug)]
583struct ExceptionBreakpoint {
584    id: String,
585    data: ExceptionBreakpointsFilter,
586    is_enabled: bool,
587}
588
589impl ExceptionBreakpoint {
590    fn render(
591        &mut self,
592        ix: usize,
593        focus_handle: FocusHandle,
594        list: WeakEntity<BreakpointList>,
595    ) -> ListItem {
596        let color = if self.is_enabled {
597            Color::Debugger
598        } else {
599            Color::Muted
600        };
601        let id = SharedString::from(&self.id);
602        let is_enabled = self.is_enabled;
603
604        ListItem::new(SharedString::from(format!(
605            "exception-breakpoint-ui-item-{}",
606            self.id
607        )))
608        .on_click({
609            let list = list.clone();
610            move |_, _, cx| {
611                list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
612                    .ok();
613            }
614        })
615        .rounded()
616        .on_secondary_mouse_down(|_, _, cx| {
617            cx.stop_propagation();
618        })
619        .start_slot(
620            div()
621                .id(SharedString::from(format!(
622                    "exception-breakpoint-ui-item-{}-click-handler",
623                    self.id
624                )))
625                .tooltip(move |window, cx| {
626                    Tooltip::for_action_in(
627                        if is_enabled {
628                            "Disable Exception Breakpoint"
629                        } else {
630                            "Enable Exception Breakpoint"
631                        },
632                        &ToggleEnableBreakpoint,
633                        &focus_handle,
634                        window,
635                        cx,
636                    )
637                })
638                .on_click({
639                    let list = list.clone();
640                    move |_, _, cx| {
641                        list.update(cx, |this, cx| {
642                            if let Some(session) = &this.session {
643                                session.update(cx, |this, cx| {
644                                    this.toggle_exception_breakpoint(&id, cx);
645                                });
646                                cx.notify();
647                            }
648                        })
649                        .ok();
650                    }
651                })
652                .cursor_pointer()
653                .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
654        )
655        .child(
656            v_flex()
657                .py_1()
658                .gap_1()
659                .min_h(px(26.))
660                .justify_center()
661                .id(("exception-breakpoint-label", ix))
662                .child(
663                    Label::new(self.data.label.clone())
664                        .size(LabelSize::Small)
665                        .line_height_style(ui::LineHeightStyle::UiLabel),
666                )
667                .when_some(self.data.description.clone(), |el, description| {
668                    el.tooltip(Tooltip::text(description))
669                }),
670        )
671    }
672}
673#[derive(Clone, Debug)]
674enum BreakpointEntryKind {
675    LineBreakpoint(LineBreakpoint),
676    ExceptionBreakpoint(ExceptionBreakpoint),
677}
678
679#[derive(Clone, Debug)]
680struct BreakpointEntry {
681    kind: BreakpointEntryKind,
682    weak: WeakEntity<BreakpointList>,
683}
684
685impl BreakpointEntry {
686    fn render(
687        &mut self,
688        ix: usize,
689        focus_handle: FocusHandle,
690        _: &mut Window,
691        _: &mut App,
692    ) -> ListItem {
693        match &mut self.kind {
694            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
695                line_breakpoint.render(ix, focus_handle, self.weak.clone())
696            }
697            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
698                exception_breakpoint.render(ix, focus_handle, self.weak.clone())
699            }
700        }
701    }
702}