1use std::{
2 ops::Range,
3 path::{Path, PathBuf},
4 sync::Arc,
5 time::Duration,
6};
7
8use dap::ExceptionBreakpointsFilter;
9use editor::Editor;
10use gpui::{
11 AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
12 UniformListScrollHandle, WeakEntity, uniform_list,
13};
14use language::Point;
15use project::{
16 Project,
17 debugger::{
18 breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
19 session::Session,
20 },
21 worktree_store::WorktreeStore,
22};
23use ui::{
24 App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
25 IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
26 ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
27 Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
28};
29use util::ResultExt;
30use workspace::Workspace;
31use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
32
33pub(crate) struct BreakpointList {
34 workspace: WeakEntity<Workspace>,
35 breakpoint_store: Entity<BreakpointStore>,
36 worktree_store: Entity<WorktreeStore>,
37 scrollbar_state: ScrollbarState,
38 breakpoints: Vec<BreakpointEntry>,
39 session: Entity<Session>,
40 hide_scrollbar_task: Option<Task<()>>,
41 show_scrollbar: bool,
42 focus_handle: FocusHandle,
43 scroll_handle: UniformListScrollHandle,
44 selected_ix: Option<usize>,
45}
46
47impl Focusable for BreakpointList {
48 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
49 self.focus_handle.clone()
50 }
51}
52
53impl BreakpointList {
54 pub(super) fn new(
55 session: Entity<Session>,
56 workspace: WeakEntity<Workspace>,
57 project: &Entity<Project>,
58 cx: &mut App,
59 ) -> Entity<Self> {
60 let project = project.read(cx);
61 let breakpoint_store = project.breakpoint_store();
62 let worktree_store = project.worktree_store();
63 let focus_handle = cx.focus_handle();
64 let scroll_handle = UniformListScrollHandle::new();
65 let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
66
67 cx.new(|_| {
68 Self {
69 breakpoint_store,
70 worktree_store,
71 scrollbar_state,
72 // list_state,
73 breakpoints: Default::default(),
74 hide_scrollbar_task: None,
75 show_scrollbar: false,
76 workspace,
77 session,
78 focus_handle,
79 scroll_handle,
80 selected_ix: None,
81 }
82 })
83 }
84
85 fn edit_line_breakpoint(
86 &mut self,
87 path: Arc<Path>,
88 row: u32,
89 action: BreakpointEditAction,
90 cx: &mut Context<Self>,
91 ) {
92 self.breakpoint_store.update(cx, |breakpoint_store, cx| {
93 if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
94 breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
95 } else {
96 log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
97 }
98 })
99 }
100
101 fn go_to_line_breakpoint(
102 &mut self,
103 path: Arc<Path>,
104 row: u32,
105 window: &mut Window,
106 cx: &mut Context<Self>,
107 ) {
108 let task = self
109 .worktree_store
110 .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
111 cx.spawn_in(window, async move |this, cx| {
112 let (worktree, relative_path) = task.await?;
113 let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
114 let item = this
115 .update_in(cx, |this, window, cx| {
116 this.workspace.update(cx, |this, cx| {
117 this.open_path((worktree_id, relative_path), None, true, window, cx)
118 })
119 })??
120 .await?;
121 if let Some(editor) = item.downcast::<Editor>() {
122 editor
123 .update_in(cx, |this, window, cx| {
124 this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
125 })
126 .ok();
127 }
128 anyhow::Ok(())
129 })
130 .detach();
131 }
132
133 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
134 self.selected_ix = ix;
135 if let Some(ix) = ix {
136 self.scroll_handle
137 .scroll_to_item(ix, ScrollStrategy::Center);
138 }
139 cx.notify();
140 }
141
142 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
143 let ix = match self.selected_ix {
144 _ if self.breakpoints.len() == 0 => None,
145 None => Some(0),
146 Some(ix) => {
147 if ix == self.breakpoints.len() - 1 {
148 Some(0)
149 } else {
150 Some(ix + 1)
151 }
152 }
153 };
154 self.select_ix(ix, cx);
155 }
156
157 fn select_previous(
158 &mut self,
159 _: &menu::SelectPrevious,
160 _window: &mut Window,
161 cx: &mut Context<Self>,
162 ) {
163 let ix = match self.selected_ix {
164 _ if self.breakpoints.len() == 0 => None,
165 None => Some(self.breakpoints.len() - 1),
166 Some(ix) => {
167 if ix == 0 {
168 Some(self.breakpoints.len() - 1)
169 } else {
170 Some(ix - 1)
171 }
172 }
173 };
174 self.select_ix(ix, cx);
175 }
176
177 fn select_first(
178 &mut self,
179 _: &menu::SelectFirst,
180 _window: &mut Window,
181 cx: &mut Context<Self>,
182 ) {
183 let ix = if self.breakpoints.len() > 0 {
184 Some(0)
185 } else {
186 None
187 };
188 self.select_ix(ix, cx);
189 }
190
191 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
192 let ix = if self.breakpoints.len() > 0 {
193 Some(self.breakpoints.len() - 1)
194 } else {
195 None
196 };
197 self.select_ix(ix, cx);
198 }
199
200 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
201 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
202 return;
203 };
204
205 match &mut entry.kind {
206 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
207 let path = line_breakpoint.breakpoint.path.clone();
208 let row = line_breakpoint.breakpoint.row;
209 self.go_to_line_breakpoint(path, row, window, cx);
210 }
211 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
212 }
213 }
214
215 fn toggle_enable_breakpoint(
216 &mut self,
217 _: &ToggleEnableBreakpoint,
218 _window: &mut Window,
219 cx: &mut Context<Self>,
220 ) {
221 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
222 return;
223 };
224
225 match &mut entry.kind {
226 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
227 let path = line_breakpoint.breakpoint.path.clone();
228 let row = line_breakpoint.breakpoint.row;
229 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
230 }
231 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
232 let id = exception_breakpoint.id.clone();
233 self.session.update(cx, |session, cx| {
234 session.toggle_exception_breakpoint(&id, cx);
235 });
236 }
237 }
238 cx.notify();
239 }
240
241 fn unset_breakpoint(
242 &mut self,
243 _: &UnsetBreakpoint,
244 _window: &mut Window,
245 cx: &mut Context<Self>,
246 ) {
247 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
248 return;
249 };
250
251 match &mut entry.kind {
252 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
253 let path = line_breakpoint.breakpoint.path.clone();
254 let row = line_breakpoint.breakpoint.row;
255 self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
256 }
257 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
258 }
259 cx.notify();
260 }
261
262 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
263 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
264 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
265 cx.background_executor()
266 .timer(SCROLLBAR_SHOW_INTERVAL)
267 .await;
268 panel
269 .update(cx, |panel, cx| {
270 panel.show_scrollbar = false;
271 cx.notify();
272 })
273 .log_err();
274 }))
275 }
276
277 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
278 let selected_ix = self.selected_ix;
279 let focus_handle = self.focus_handle.clone();
280 uniform_list(
281 "breakpoint-list",
282 self.breakpoints.len(),
283 cx.processor(move |this, range: Range<usize>, 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}