breakpoint_list.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    time::Duration,
  4};
  5
  6use dap::ExceptionBreakpointsFilter;
  7use editor::Editor;
  8use gpui::{
  9    AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity,
 10    list,
 11};
 12use language::Point;
 13use project::{
 14    Project,
 15    debugger::{
 16        breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
 17        session::Session,
 18    },
 19    worktree_store::WorktreeStore,
 20};
 21use ui::{
 22    App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
 23    IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
 24    Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
 25    div, h_flex, px, v_flex,
 26};
 27use util::{ResultExt, maybe};
 28use workspace::Workspace;
 29
 30pub(crate) struct BreakpointList {
 31    workspace: WeakEntity<Workspace>,
 32    breakpoint_store: Entity<BreakpointStore>,
 33    worktree_store: Entity<WorktreeStore>,
 34    list_state: ListState,
 35    scrollbar_state: ScrollbarState,
 36    breakpoints: Vec<BreakpointEntry>,
 37    session: Entity<Session>,
 38    hide_scrollbar_task: Option<Task<()>>,
 39    show_scrollbar: bool,
 40    focus_handle: FocusHandle,
 41}
 42
 43impl Focusable for BreakpointList {
 44    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 45        self.focus_handle.clone()
 46    }
 47}
 48
 49impl BreakpointList {
 50    pub(super) fn new(
 51        session: Entity<Session>,
 52        workspace: WeakEntity<Workspace>,
 53        project: &Entity<Project>,
 54        cx: &mut App,
 55    ) -> Entity<Self> {
 56        let project = project.read(cx);
 57        let breakpoint_store = project.breakpoint_store();
 58        let worktree_store = project.worktree_store();
 59
 60        cx.new(|cx| {
 61            let weak: gpui::WeakEntity<Self> = cx.weak_entity();
 62            let list_state = ListState::new(
 63                0,
 64                gpui::ListAlignment::Top,
 65                px(1000.),
 66                move |ix, window, cx| {
 67                    let Ok(Some(breakpoint)) =
 68                        weak.update(cx, |this, _| this.breakpoints.get(ix).cloned())
 69                    else {
 70                        return div().into_any_element();
 71                    };
 72
 73                    breakpoint.render(window, cx).into_any_element()
 74                },
 75            );
 76            Self {
 77                breakpoint_store,
 78                worktree_store,
 79                scrollbar_state: ScrollbarState::new(list_state.clone()),
 80                list_state,
 81                breakpoints: Default::default(),
 82                hide_scrollbar_task: None,
 83                show_scrollbar: false,
 84                workspace,
 85                session,
 86                focus_handle: cx.focus_handle(),
 87            }
 88        })
 89    }
 90
 91    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 92        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 93        self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
 94            cx.background_executor()
 95                .timer(SCROLLBAR_SHOW_INTERVAL)
 96                .await;
 97            panel
 98                .update(cx, |panel, cx| {
 99                    panel.show_scrollbar = false;
100                    cx.notify();
101                })
102                .log_err();
103        }))
104    }
105
106    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
107        if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
108            return None;
109        }
110        Some(
111            div()
112                .occlude()
113                .id("breakpoint-list-vertical-scrollbar")
114                .on_mouse_move(cx.listener(|_, _, _, cx| {
115                    cx.notify();
116                    cx.stop_propagation()
117                }))
118                .on_hover(|_, _, cx| {
119                    cx.stop_propagation();
120                })
121                .on_any_mouse_down(|_, _, cx| {
122                    cx.stop_propagation();
123                })
124                .on_mouse_up(
125                    MouseButton::Left,
126                    cx.listener(|_, _, _, cx| {
127                        cx.stop_propagation();
128                    }),
129                )
130                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
131                    cx.notify();
132                }))
133                .h_full()
134                .absolute()
135                .right_1()
136                .top_1()
137                .bottom_0()
138                .w(px(12.))
139                .cursor_default()
140                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
141        )
142    }
143}
144impl Render for BreakpointList {
145    fn render(
146        &mut self,
147        _window: &mut ui::Window,
148        cx: &mut ui::Context<Self>,
149    ) -> impl ui::IntoElement {
150        let old_len = self.breakpoints.len();
151        let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
152        self.breakpoints.clear();
153        let weak = cx.weak_entity();
154        let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
155            let relative_worktree_path = self
156                .worktree_store
157                .read(cx)
158                .find_worktree(&path, cx)
159                .and_then(|(worktree, relative_path)| {
160                    worktree
161                        .read(cx)
162                        .is_visible()
163                        .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
164                });
165            breakpoints.sort_by_key(|breakpoint| breakpoint.row);
166            let weak = weak.clone();
167            breakpoints.into_iter().filter_map(move |breakpoint| {
168                debug_assert_eq!(&path, &breakpoint.path);
169                let file_name = breakpoint.path.file_name()?;
170
171                let dir = relative_worktree_path
172                    .clone()
173                    .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
174                    .parent()
175                    .and_then(|parent| {
176                        parent
177                            .to_str()
178                            .map(ToOwned::to_owned)
179                            .map(SharedString::from)
180                    });
181                let name = file_name
182                    .to_str()
183                    .map(ToOwned::to_owned)
184                    .map(SharedString::from)?;
185                let weak = weak.clone();
186                let line = format!("Line {}", breakpoint.row + 1).into();
187                Some(BreakpointEntry {
188                    kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
189                        name,
190                        dir,
191                        line,
192                        breakpoint,
193                    }),
194                    weak,
195                })
196            })
197        });
198        let exception_breakpoints =
199            self.session
200                .read(cx)
201                .exception_breakpoints()
202                .map(|(data, is_enabled)| BreakpointEntry {
203                    kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
204                        id: data.filter.clone(),
205                        data: data.clone(),
206                        is_enabled: *is_enabled,
207                    }),
208                    weak: weak.clone(),
209                });
210        self.breakpoints
211            .extend(breakpoints.chain(exception_breakpoints));
212        if self.breakpoints.len() != old_len {
213            self.list_state.reset(self.breakpoints.len());
214        }
215        v_flex()
216            .id("breakpoint-list")
217            .track_focus(&self.focus_handle)
218            .on_hover(cx.listener(|this, hovered, window, cx| {
219                if *hovered {
220                    this.show_scrollbar = true;
221                    this.hide_scrollbar_task.take();
222                    cx.notify();
223                } else if !this.focus_handle.contains_focused(window, cx) {
224                    this.hide_scrollbar(window, cx);
225                }
226            }))
227            .size_full()
228            .m_0p5()
229            .child(list(self.list_state.clone()).flex_grow())
230            .children(self.render_vertical_scrollbar(cx))
231    }
232}
233#[derive(Clone, Debug)]
234struct LineBreakpoint {
235    name: SharedString,
236    dir: Option<SharedString>,
237    line: SharedString,
238    breakpoint: SourceBreakpoint,
239}
240
241impl LineBreakpoint {
242    fn render(self, weak: WeakEntity<BreakpointList>) -> ListItem {
243        let LineBreakpoint {
244            name,
245            dir,
246            line,
247            breakpoint,
248        } = self;
249        let icon_name = if breakpoint.state.is_enabled() {
250            IconName::DebugBreakpoint
251        } else {
252            IconName::DebugDisabledBreakpoint
253        };
254        let path = breakpoint.path;
255        let row = breakpoint.row;
256        let indicator = div()
257            .id(SharedString::from(format!(
258                "breakpoint-ui-toggle-{:?}/{}:{}",
259                dir, name, line
260            )))
261            .cursor_pointer()
262            .tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
263                "Disable Breakpoint"
264            } else {
265                "Enable Breakpoint"
266            }))
267            .on_click({
268                let weak = weak.clone();
269                let path = path.clone();
270                move |_, _, cx| {
271                    weak.update(cx, |this, cx| {
272                        this.breakpoint_store.update(cx, |this, cx| {
273                            if let Some((buffer, breakpoint)) =
274                                this.breakpoint_at_row(&path, row, cx)
275                            {
276                                this.toggle_breakpoint(
277                                    buffer,
278                                    breakpoint,
279                                    BreakpointEditAction::InvertState,
280                                    cx,
281                                );
282                            } else {
283                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
284                            }
285                        })
286                    })
287                    .ok();
288                }
289            })
290            .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
291            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
292        ListItem::new(SharedString::from(format!(
293            "breakpoint-ui-item-{:?}/{}:{}",
294            dir, name, line
295        )))
296        .start_slot(indicator)
297        .rounded()
298        .on_secondary_mouse_down(|_, _, cx| {
299            cx.stop_propagation();
300        })
301        .end_hover_slot(
302            IconButton::new(
303                SharedString::from(format!(
304                    "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
305                    dir, name, line
306                )),
307                IconName::Close,
308            )
309            .on_click({
310                let weak = weak.clone();
311                let path = path.clone();
312                move |_, _, cx| {
313                    weak.update(cx, |this, cx| {
314                        this.breakpoint_store.update(cx, |this, cx| {
315                            if let Some((buffer, breakpoint)) =
316                                this.breakpoint_at_row(&path, row, cx)
317                            {
318                                this.toggle_breakpoint(
319                                    buffer,
320                                    breakpoint,
321                                    BreakpointEditAction::Toggle,
322                                    cx,
323                                );
324                            } else {
325                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
326                            }
327                        })
328                    })
329                    .ok();
330                }
331            })
332            .icon_size(ui::IconSize::XSmall),
333        )
334        .child(
335            v_flex()
336                .id(SharedString::from(format!(
337                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
338                    dir, name, line
339                )))
340                .on_click(move |_, window, cx| {
341                    let path = path.clone();
342                    let weak = weak.clone();
343                    let row = breakpoint.row;
344                    maybe!({
345                        let task = weak
346                            .update(cx, |this, cx| {
347                                this.worktree_store.update(cx, |this, cx| {
348                                    this.find_or_create_worktree(path, false, cx)
349                                })
350                            })
351                            .ok()?;
352                        window
353                            .spawn(cx, async move |cx| {
354                                let (worktree, relative_path) = task.await?;
355                                let worktree_id = worktree.update(cx, |this, _| this.id())?;
356                                let item = weak
357                                    .update_in(cx, |this, window, cx| {
358                                        this.workspace.update(cx, |this, cx| {
359                                            this.open_path(
360                                                (worktree_id, relative_path),
361                                                None,
362                                                true,
363                                                window,
364                                                cx,
365                                            )
366                                        })
367                                    })??
368                                    .await?;
369                                if let Some(editor) = item.downcast::<Editor>() {
370                                    editor
371                                        .update_in(cx, |this, window, cx| {
372                                            this.go_to_singleton_buffer_point(
373                                                Point { row, column: 0 },
374                                                window,
375                                                cx,
376                                            );
377                                        })
378                                        .ok();
379                                }
380                                Result::<_, anyhow::Error>::Ok(())
381                            })
382                            .detach();
383
384                        Some(())
385                    });
386                })
387                .cursor_pointer()
388                .py_1()
389                .items_center()
390                .child(
391                    h_flex()
392                        .gap_1()
393                        .child(
394                            Label::new(name)
395                                .size(LabelSize::Small)
396                                .line_height_style(ui::LineHeightStyle::UiLabel),
397                        )
398                        .children(dir.map(|dir| {
399                            Label::new(dir)
400                                .color(Color::Muted)
401                                .size(LabelSize::Small)
402                                .line_height_style(ui::LineHeightStyle::UiLabel)
403                        })),
404                )
405                .child(
406                    Label::new(line)
407                        .size(LabelSize::XSmall)
408                        .color(Color::Muted)
409                        .line_height_style(ui::LineHeightStyle::UiLabel),
410                ),
411        )
412    }
413}
414#[derive(Clone, Debug)]
415struct ExceptionBreakpoint {
416    id: String,
417    data: ExceptionBreakpointsFilter,
418    is_enabled: bool,
419}
420
421impl ExceptionBreakpoint {
422    fn render(self, list: WeakEntity<BreakpointList>) -> ListItem {
423        let color = if self.is_enabled {
424            Color::Debugger
425        } else {
426            Color::Muted
427        };
428        let id = SharedString::from(&self.id);
429        ListItem::new(SharedString::from(format!(
430            "exception-breakpoint-ui-item-{}",
431            self.id
432        )))
433        .rounded()
434        .on_secondary_mouse_down(|_, _, cx| {
435            cx.stop_propagation();
436        })
437        .start_slot(
438            div()
439                .id(SharedString::from(format!(
440                    "exception-breakpoint-ui-item-{}-click-handler",
441                    self.id
442                )))
443                .tooltip(Tooltip::text(if self.is_enabled {
444                    "Disable Exception Breakpoint"
445                } else {
446                    "Enable Exception Breakpoint"
447                }))
448                .on_click(move |_, _, cx| {
449                    list.update(cx, |this, cx| {
450                        this.session.update(cx, |this, cx| {
451                            this.toggle_exception_breakpoint(&id, cx);
452                        });
453                        cx.notify();
454                    })
455                    .ok();
456                })
457                .cursor_pointer()
458                .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
459        )
460        .child(
461            div()
462                .py_1()
463                .gap_1()
464                .child(
465                    Label::new(self.data.label)
466                        .size(LabelSize::Small)
467                        .line_height_style(ui::LineHeightStyle::UiLabel),
468                )
469                .children(self.data.description.map(|description| {
470                    Label::new(description)
471                        .size(LabelSize::XSmall)
472                        .line_height_style(ui::LineHeightStyle::UiLabel)
473                        .color(Color::Muted)
474                })),
475        )
476    }
477}
478#[derive(Clone, Debug)]
479enum BreakpointEntryKind {
480    LineBreakpoint(LineBreakpoint),
481    ExceptionBreakpoint(ExceptionBreakpoint),
482}
483
484#[derive(Clone, Debug)]
485struct BreakpointEntry {
486    kind: BreakpointEntryKind,
487    weak: WeakEntity<BreakpointList>,
488}
489impl RenderOnce for BreakpointEntry {
490    fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement {
491        match self.kind {
492            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
493                line_breakpoint.render(self.weak)
494            }
495            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
496                exception_breakpoint.render(self.weak)
497            }
498        }
499    }
500}