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}