1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4 time::Duration,
5};
6
7use dap::ExceptionBreakpointsFilter;
8use editor::Editor;
9use gpui::{
10 AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
11 UniformListScrollHandle, WeakEntity, uniform_list,
12};
13use language::Point;
14use project::{
15 Project,
16 debugger::{
17 breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
18 session::Session,
19 },
20 worktree_store::WorktreeStore,
21};
22use ui::{
23 App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
24 IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
25 ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
26 Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
27};
28use util::ResultExt;
29use workspace::Workspace;
30use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
31
32pub(crate) struct BreakpointList {
33 workspace: WeakEntity<Workspace>,
34 breakpoint_store: Entity<BreakpointStore>,
35 worktree_store: Entity<WorktreeStore>,
36 scrollbar_state: ScrollbarState,
37 breakpoints: Vec<BreakpointEntry>,
38 session: Entity<Session>,
39 hide_scrollbar_task: Option<Task<()>>,
40 show_scrollbar: bool,
41 focus_handle: FocusHandle,
42 scroll_handle: UniformListScrollHandle,
43 selected_ix: Option<usize>,
44}
45
46impl Focusable for BreakpointList {
47 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
48 self.focus_handle.clone()
49 }
50}
51
52impl BreakpointList {
53 pub(super) fn new(
54 session: Entity<Session>,
55 workspace: WeakEntity<Workspace>,
56 project: &Entity<Project>,
57 cx: &mut App,
58 ) -> Entity<Self> {
59 let project = project.read(cx);
60 let breakpoint_store = project.breakpoint_store();
61 let worktree_store = project.worktree_store();
62 let focus_handle = cx.focus_handle();
63 let scroll_handle = UniformListScrollHandle::new();
64 let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
65
66 cx.new(|_| {
67 Self {
68 breakpoint_store,
69 worktree_store,
70 scrollbar_state,
71 // list_state,
72 breakpoints: Default::default(),
73 hide_scrollbar_task: None,
74 show_scrollbar: false,
75 workspace,
76 session,
77 focus_handle,
78 scroll_handle,
79 selected_ix: None,
80 }
81 })
82 }
83
84 fn edit_line_breakpoint(
85 &mut self,
86 path: Arc<Path>,
87 row: u32,
88 action: BreakpointEditAction,
89 cx: &mut Context<Self>,
90 ) {
91 self.breakpoint_store.update(cx, |breakpoint_store, cx| {
92 if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
93 breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
94 } else {
95 log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
96 }
97 })
98 }
99
100 fn go_to_line_breakpoint(
101 &mut self,
102 path: Arc<Path>,
103 row: u32,
104 window: &mut Window,
105 cx: &mut Context<Self>,
106 ) {
107 let task = self
108 .worktree_store
109 .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
110 cx.spawn_in(window, async move |this, cx| {
111 let (worktree, relative_path) = task.await?;
112 let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
113 let item = this
114 .update_in(cx, |this, window, cx| {
115 this.workspace.update(cx, |this, cx| {
116 this.open_path((worktree_id, relative_path), None, true, window, cx)
117 })
118 })??
119 .await?;
120 if let Some(editor) = item.downcast::<Editor>() {
121 editor
122 .update_in(cx, |this, window, cx| {
123 this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
124 })
125 .ok();
126 }
127 anyhow::Ok(())
128 })
129 .detach();
130 }
131
132 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
133 self.selected_ix = ix;
134 if let Some(ix) = ix {
135 self.scroll_handle
136 .scroll_to_item(ix, ScrollStrategy::Center);
137 }
138 cx.notify();
139 }
140
141 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
142 let ix = match self.selected_ix {
143 _ if self.breakpoints.len() == 0 => None,
144 None => Some(0),
145 Some(ix) => {
146 if ix == self.breakpoints.len() - 1 {
147 Some(0)
148 } else {
149 Some(ix + 1)
150 }
151 }
152 };
153 self.select_ix(ix, cx);
154 }
155
156 fn select_previous(
157 &mut self,
158 _: &menu::SelectPrevious,
159 _window: &mut Window,
160 cx: &mut Context<Self>,
161 ) {
162 let ix = match self.selected_ix {
163 _ if self.breakpoints.len() == 0 => None,
164 None => Some(self.breakpoints.len() - 1),
165 Some(ix) => {
166 if ix == 0 {
167 Some(self.breakpoints.len() - 1)
168 } else {
169 Some(ix - 1)
170 }
171 }
172 };
173 self.select_ix(ix, cx);
174 }
175
176 fn select_first(
177 &mut self,
178 _: &menu::SelectFirst,
179 _window: &mut Window,
180 cx: &mut Context<Self>,
181 ) {
182 let ix = if self.breakpoints.len() > 0 {
183 Some(0)
184 } else {
185 None
186 };
187 self.select_ix(ix, cx);
188 }
189
190 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
191 let ix = if self.breakpoints.len() > 0 {
192 Some(self.breakpoints.len() - 1)
193 } else {
194 None
195 };
196 self.select_ix(ix, cx);
197 }
198
199 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
200 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
201 return;
202 };
203
204 match &mut entry.kind {
205 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
206 let path = line_breakpoint.breakpoint.path.clone();
207 let row = line_breakpoint.breakpoint.row;
208 self.go_to_line_breakpoint(path, row, window, cx);
209 }
210 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
211 }
212 }
213
214 fn toggle_enable_breakpoint(
215 &mut self,
216 _: &ToggleEnableBreakpoint,
217 _window: &mut Window,
218 cx: &mut Context<Self>,
219 ) {
220 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
221 return;
222 };
223
224 match &mut entry.kind {
225 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
226 let path = line_breakpoint.breakpoint.path.clone();
227 let row = line_breakpoint.breakpoint.row;
228 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
229 }
230 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
231 let id = exception_breakpoint.id.clone();
232 self.session.update(cx, |session, cx| {
233 session.toggle_exception_breakpoint(&id, cx);
234 });
235 }
236 }
237 cx.notify();
238 }
239
240 fn unset_breakpoint(
241 &mut self,
242 _: &UnsetBreakpoint,
243 _window: &mut Window,
244 cx: &mut Context<Self>,
245 ) {
246 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
247 return;
248 };
249
250 match &mut entry.kind {
251 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
252 let path = line_breakpoint.breakpoint.path.clone();
253 let row = line_breakpoint.breakpoint.row;
254 self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
255 }
256 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
257 }
258 cx.notify();
259 }
260
261 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
262 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
263 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
264 cx.background_executor()
265 .timer(SCROLLBAR_SHOW_INTERVAL)
266 .await;
267 panel
268 .update(cx, |panel, cx| {
269 panel.show_scrollbar = false;
270 cx.notify();
271 })
272 .log_err();
273 }))
274 }
275
276 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
277 let selected_ix = self.selected_ix;
278 let focus_handle = self.focus_handle.clone();
279 uniform_list(
280 cx.entity(),
281 "breakpoint-list",
282 self.breakpoints.len(),
283 move |this, range, window, cx| {
284 range
285 .clone()
286 .zip(&mut this.breakpoints[range])
287 .map(|(ix, breakpoint)| {
288 breakpoint
289 .render(ix, focus_handle.clone(), window, cx)
290 .toggle_state(Some(ix) == selected_ix)
291 .into_any_element()
292 })
293 .collect()
294 },
295 )
296 .track_scroll(self.scroll_handle.clone())
297 .flex_grow()
298 }
299
300 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
301 if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
302 return None;
303 }
304 Some(
305 div()
306 .occlude()
307 .id("breakpoint-list-vertical-scrollbar")
308 .on_mouse_move(cx.listener(|_, _, _, cx| {
309 cx.notify();
310 cx.stop_propagation()
311 }))
312 .on_hover(|_, _, cx| {
313 cx.stop_propagation();
314 })
315 .on_any_mouse_down(|_, _, cx| {
316 cx.stop_propagation();
317 })
318 .on_mouse_up(
319 MouseButton::Left,
320 cx.listener(|_, _, _, cx| {
321 cx.stop_propagation();
322 }),
323 )
324 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
325 cx.notify();
326 }))
327 .h_full()
328 .absolute()
329 .right_1()
330 .top_1()
331 .bottom_0()
332 .w(px(12.))
333 .cursor_default()
334 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
335 )
336 }
337}
338impl Render for BreakpointList {
339 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
340 // let old_len = self.breakpoints.len();
341 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
342 self.breakpoints.clear();
343 let weak = cx.weak_entity();
344 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
345 let relative_worktree_path = self
346 .worktree_store
347 .read(cx)
348 .find_worktree(&path, cx)
349 .and_then(|(worktree, relative_path)| {
350 worktree
351 .read(cx)
352 .is_visible()
353 .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
354 });
355 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
356 let weak = weak.clone();
357 breakpoints.into_iter().filter_map(move |breakpoint| {
358 debug_assert_eq!(&path, &breakpoint.path);
359 let file_name = breakpoint.path.file_name()?;
360
361 let dir = relative_worktree_path
362 .clone()
363 .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
364 .parent()
365 .and_then(|parent| {
366 parent
367 .to_str()
368 .map(ToOwned::to_owned)
369 .map(SharedString::from)
370 });
371 let name = file_name
372 .to_str()
373 .map(ToOwned::to_owned)
374 .map(SharedString::from)?;
375 let weak = weak.clone();
376 let line = breakpoint.row + 1;
377 Some(BreakpointEntry {
378 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
379 name,
380 dir,
381 line,
382 breakpoint,
383 }),
384 weak,
385 })
386 })
387 });
388 let exception_breakpoints =
389 self.session
390 .read(cx)
391 .exception_breakpoints()
392 .map(|(data, is_enabled)| BreakpointEntry {
393 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
394 id: data.filter.clone(),
395 data: data.clone(),
396 is_enabled: *is_enabled,
397 }),
398 weak: weak.clone(),
399 });
400 self.breakpoints
401 .extend(breakpoints.chain(exception_breakpoints));
402 v_flex()
403 .id("breakpoint-list")
404 .key_context("BreakpointList")
405 .track_focus(&self.focus_handle)
406 .on_hover(cx.listener(|this, hovered, window, cx| {
407 if *hovered {
408 this.show_scrollbar = true;
409 this.hide_scrollbar_task.take();
410 cx.notify();
411 } else if !this.focus_handle.contains_focused(window, cx) {
412 this.hide_scrollbar(window, cx);
413 }
414 }))
415 .on_action(cx.listener(Self::select_next))
416 .on_action(cx.listener(Self::select_previous))
417 .on_action(cx.listener(Self::select_first))
418 .on_action(cx.listener(Self::select_last))
419 .on_action(cx.listener(Self::confirm))
420 .on_action(cx.listener(Self::toggle_enable_breakpoint))
421 .on_action(cx.listener(Self::unset_breakpoint))
422 .size_full()
423 .m_0p5()
424 .child(self.render_list(window, cx))
425 .children(self.render_vertical_scrollbar(cx))
426 }
427}
428#[derive(Clone, Debug)]
429struct LineBreakpoint {
430 name: SharedString,
431 dir: Option<SharedString>,
432 line: u32,
433 breakpoint: SourceBreakpoint,
434}
435
436impl LineBreakpoint {
437 fn render(
438 &mut self,
439 ix: usize,
440 focus_handle: FocusHandle,
441 weak: WeakEntity<BreakpointList>,
442 ) -> ListItem {
443 let icon_name = if self.breakpoint.state.is_enabled() {
444 IconName::DebugBreakpoint
445 } else {
446 IconName::DebugDisabledBreakpoint
447 };
448 let path = self.breakpoint.path.clone();
449 let row = self.breakpoint.row;
450 let is_enabled = self.breakpoint.state.is_enabled();
451 let indicator = div()
452 .id(SharedString::from(format!(
453 "breakpoint-ui-toggle-{:?}/{}:{}",
454 self.dir, self.name, self.line
455 )))
456 .cursor_pointer()
457 .tooltip({
458 let focus_handle = focus_handle.clone();
459 move |window, cx| {
460 Tooltip::for_action_in(
461 if is_enabled {
462 "Disable Breakpoint"
463 } else {
464 "Enable Breakpoint"
465 },
466 &ToggleEnableBreakpoint,
467 &focus_handle,
468 window,
469 cx,
470 )
471 }
472 })
473 .on_click({
474 let weak = weak.clone();
475 let path = path.clone();
476 move |_, _, cx| {
477 weak.update(cx, |breakpoint_list, cx| {
478 breakpoint_list.edit_line_breakpoint(
479 path.clone(),
480 row,
481 BreakpointEditAction::InvertState,
482 cx,
483 );
484 })
485 .ok();
486 }
487 })
488 .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
489 .on_mouse_down(MouseButton::Left, move |_, _, _| {});
490 ListItem::new(SharedString::from(format!(
491 "breakpoint-ui-item-{:?}/{}:{}",
492 self.dir, self.name, self.line
493 )))
494 .on_click({
495 let weak = weak.clone();
496 move |_, _, cx| {
497 weak.update(cx, |breakpoint_list, cx| {
498 breakpoint_list.select_ix(Some(ix), cx);
499 })
500 .ok();
501 }
502 })
503 .start_slot(indicator)
504 .rounded()
505 .on_secondary_mouse_down(|_, _, cx| {
506 cx.stop_propagation();
507 })
508 .end_hover_slot(
509 IconButton::new(
510 SharedString::from(format!(
511 "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
512 self.dir, self.name, self.line
513 )),
514 IconName::Close,
515 )
516 .on_click({
517 let weak = weak.clone();
518 let path = path.clone();
519 move |_, _, cx| {
520 weak.update(cx, |breakpoint_list, cx| {
521 breakpoint_list.edit_line_breakpoint(
522 path.clone(),
523 row,
524 BreakpointEditAction::Toggle,
525 cx,
526 );
527 })
528 .ok();
529 }
530 })
531 .tooltip(move |window, cx| {
532 Tooltip::for_action_in(
533 "Unset Breakpoint",
534 &UnsetBreakpoint,
535 &focus_handle,
536 window,
537 cx,
538 )
539 })
540 .icon_size(ui::IconSize::Indicator),
541 )
542 .child(
543 v_flex()
544 .py_1()
545 .gap_1()
546 .min_h(px(22.))
547 .justify_center()
548 .id(SharedString::from(format!(
549 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
550 self.dir, self.name, self.line
551 )))
552 .on_click(move |_, window, cx| {
553 weak.update(cx, |breakpoint_list, cx| {
554 breakpoint_list.select_ix(Some(ix), cx);
555 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
556 })
557 .ok();
558 })
559 .cursor_pointer()
560 .child(
561 h_flex()
562 .gap_1()
563 .child(
564 Label::new(format!("{}:{}", self.name, self.line))
565 .size(LabelSize::Small)
566 .line_height_style(ui::LineHeightStyle::UiLabel),
567 )
568 .children(self.dir.clone().map(|dir| {
569 Label::new(dir)
570 .color(Color::Muted)
571 .size(LabelSize::Small)
572 .line_height_style(ui::LineHeightStyle::UiLabel)
573 })),
574 ),
575 )
576 }
577}
578#[derive(Clone, Debug)]
579struct ExceptionBreakpoint {
580 id: String,
581 data: ExceptionBreakpointsFilter,
582 is_enabled: bool,
583}
584
585impl ExceptionBreakpoint {
586 fn render(
587 &mut self,
588 ix: usize,
589 focus_handle: FocusHandle,
590 list: WeakEntity<BreakpointList>,
591 ) -> ListItem {
592 let color = if self.is_enabled {
593 Color::Debugger
594 } else {
595 Color::Muted
596 };
597 let id = SharedString::from(&self.id);
598 let is_enabled = self.is_enabled;
599
600 ListItem::new(SharedString::from(format!(
601 "exception-breakpoint-ui-item-{}",
602 self.id
603 )))
604 .on_click({
605 let list = list.clone();
606 move |_, _, cx| {
607 list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
608 .ok();
609 }
610 })
611 .rounded()
612 .on_secondary_mouse_down(|_, _, cx| {
613 cx.stop_propagation();
614 })
615 .start_slot(
616 div()
617 .id(SharedString::from(format!(
618 "exception-breakpoint-ui-item-{}-click-handler",
619 self.id
620 )))
621 .tooltip(move |window, cx| {
622 Tooltip::for_action_in(
623 if is_enabled {
624 "Disable Exception Breakpoint"
625 } else {
626 "Enable Exception Breakpoint"
627 },
628 &ToggleEnableBreakpoint,
629 &focus_handle,
630 window,
631 cx,
632 )
633 })
634 .on_click({
635 let list = list.clone();
636 move |_, _, cx| {
637 list.update(cx, |this, cx| {
638 this.session.update(cx, |this, cx| {
639 this.toggle_exception_breakpoint(&id, cx);
640 });
641 cx.notify();
642 })
643 .ok();
644 }
645 })
646 .cursor_pointer()
647 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
648 )
649 .child(
650 v_flex()
651 .py_1()
652 .gap_1()
653 .min_h(px(22.))
654 .justify_center()
655 .id(("exception-breakpoint-label", ix))
656 .child(
657 Label::new(self.data.label.clone())
658 .size(LabelSize::Small)
659 .line_height_style(ui::LineHeightStyle::UiLabel),
660 )
661 .when_some(self.data.description.clone(), |el, description| {
662 el.tooltip(Tooltip::text(description))
663 }),
664 )
665 }
666}
667#[derive(Clone, Debug)]
668enum BreakpointEntryKind {
669 LineBreakpoint(LineBreakpoint),
670 ExceptionBreakpoint(ExceptionBreakpoint),
671}
672
673#[derive(Clone, Debug)]
674struct BreakpointEntry {
675 kind: BreakpointEntryKind,
676 weak: WeakEntity<BreakpointList>,
677}
678
679impl BreakpointEntry {
680 fn render(
681 &mut self,
682 ix: usize,
683 focus_handle: FocusHandle,
684 _: &mut Window,
685 _: &mut App,
686 ) -> ListItem {
687 match &mut self.kind {
688 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
689 line_breakpoint.render(ix, focus_handle, self.weak.clone())
690 }
691 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
692 exception_breakpoint.render(ix, focus_handle, self.weak.clone())
693 }
694 }
695 }
696}