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}