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}