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