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, Window, div,
 25    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_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            .on_click({
263                let weak = weak.clone();
264                let path = path.clone();
265                move |_, _, cx| {
266                    weak.update(cx, |this, cx| {
267                        this.breakpoint_store.update(cx, |this, cx| {
268                            if let Some((buffer, breakpoint)) =
269                                this.breakpoint_at_row(&path, row, cx)
270                            {
271                                this.toggle_breakpoint(
272                                    buffer,
273                                    breakpoint,
274                                    BreakpointEditAction::InvertState,
275                                    cx,
276                                );
277                            } else {
278                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
279                            }
280                        })
281                    })
282                    .ok();
283                }
284            })
285            .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
286            .on_mouse_down(MouseButton::Left, move |_, _, _| {});
287        ListItem::new(SharedString::from(format!(
288            "breakpoint-ui-item-{:?}/{}:{}",
289            dir, name, line
290        )))
291        .start_slot(indicator)
292        .rounded()
293        .end_hover_slot(
294            IconButton::new(
295                SharedString::from(format!(
296                    "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
297                    dir, name, line
298                )),
299                IconName::Close,
300            )
301            .on_click({
302                let weak = weak.clone();
303                let path = path.clone();
304                move |_, _, cx| {
305                    weak.update(cx, |this, cx| {
306                        this.breakpoint_store.update(cx, |this, cx| {
307                            if let Some((buffer, breakpoint)) =
308                                this.breakpoint_at_row(&path, row, cx)
309                            {
310                                this.toggle_breakpoint(
311                                    buffer,
312                                    breakpoint,
313                                    BreakpointEditAction::Toggle,
314                                    cx,
315                                );
316                            } else {
317                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
318                            }
319                        })
320                    })
321                    .ok();
322                }
323            })
324            .icon_size(ui::IconSize::XSmall),
325        )
326        .child(
327            v_flex()
328                .id(SharedString::from(format!(
329                    "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
330                    dir, name, line
331                )))
332                .on_click(move |_, window, cx| {
333                    let path = path.clone();
334                    let weak = weak.clone();
335                    let row = breakpoint.row;
336                    maybe!({
337                        let task = weak
338                            .update(cx, |this, cx| {
339                                this.worktree_store.update(cx, |this, cx| {
340                                    this.find_or_create_worktree(path, false, cx)
341                                })
342                            })
343                            .ok()?;
344                        window
345                            .spawn(cx, async move |cx| {
346                                let (worktree, relative_path) = task.await?;
347                                let worktree_id = worktree.update(cx, |this, _| this.id())?;
348                                let item = weak
349                                    .update_in(cx, |this, window, cx| {
350                                        this.workspace.update(cx, |this, cx| {
351                                            this.open_path(
352                                                (worktree_id, relative_path),
353                                                None,
354                                                true,
355                                                window,
356                                                cx,
357                                            )
358                                        })
359                                    })??
360                                    .await?;
361                                if let Some(editor) = item.downcast::<Editor>() {
362                                    editor
363                                        .update_in(cx, |this, window, cx| {
364                                            this.go_to_singleton_buffer_point(
365                                                Point { row, column: 0 },
366                                                window,
367                                                cx,
368                                            );
369                                        })
370                                        .ok();
371                                }
372                                Result::<_, anyhow::Error>::Ok(())
373                            })
374                            .detach();
375
376                        Some(())
377                    });
378                })
379                .cursor_pointer()
380                .py_1()
381                .items_center()
382                .child(
383                    h_flex()
384                        .gap_1()
385                        .child(
386                            Label::new(name)
387                                .size(LabelSize::Small)
388                                .line_height_style(ui::LineHeightStyle::UiLabel),
389                        )
390                        .children(dir.map(|dir| {
391                            Label::new(dir)
392                                .color(Color::Muted)
393                                .size(LabelSize::Small)
394                                .line_height_style(ui::LineHeightStyle::UiLabel)
395                        })),
396                )
397                .child(
398                    Label::new(line)
399                        .size(LabelSize::XSmall)
400                        .color(Color::Muted)
401                        .line_height_style(ui::LineHeightStyle::UiLabel),
402                ),
403        )
404    }
405}
406#[derive(Clone, Debug)]
407struct ExceptionBreakpoint {
408    id: String,
409    data: ExceptionBreakpointsFilter,
410    is_enabled: bool,
411}
412
413impl ExceptionBreakpoint {
414    fn render(self, list: WeakEntity<BreakpointList>) -> ListItem {
415        let color = if self.is_enabled {
416            Color::Debugger
417        } else {
418            Color::Muted
419        };
420        let id = SharedString::from(&self.id);
421        ListItem::new(SharedString::from(format!(
422            "exception-breakpoint-ui-item-{}",
423            self.id
424        )))
425        .rounded()
426        .start_slot(
427            div()
428                .id(SharedString::from(format!(
429                    "exception-breakpoint-ui-item-{}-click-handler",
430                    self.id
431                )))
432                .on_click(move |_, _, cx| {
433                    list.update(cx, |this, cx| {
434                        this.session.update(cx, |this, cx| {
435                            this.toggle_exception_breakpoint(&id, cx);
436                        });
437                        cx.notify();
438                    })
439                    .ok();
440                })
441                .cursor_pointer()
442                .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
443        )
444        .child(
445            div()
446                .py_1()
447                .gap_1()
448                .child(
449                    Label::new(self.data.label)
450                        .size(LabelSize::Small)
451                        .line_height_style(ui::LineHeightStyle::UiLabel),
452                )
453                .children(self.data.description.map(|description| {
454                    Label::new(description)
455                        .size(LabelSize::XSmall)
456                        .line_height_style(ui::LineHeightStyle::UiLabel)
457                        .color(Color::Muted)
458                })),
459        )
460    }
461}
462#[derive(Clone, Debug)]
463enum BreakpointEntryKind {
464    LineBreakpoint(LineBreakpoint),
465    ExceptionBreakpoint(ExceptionBreakpoint),
466}
467
468#[derive(Clone, Debug)]
469struct BreakpointEntry {
470    kind: BreakpointEntryKind,
471    weak: WeakEntity<BreakpointList>,
472}
473impl RenderOnce for BreakpointEntry {
474    fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement {
475        match self.kind {
476            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
477                line_breakpoint.render(self.weak)
478            }
479            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
480                exception_breakpoint.render(self.weak)
481            }
482        }
483    }
484}