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: Option<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(crate) fn new(
55 session: Option<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(|_| Self {
68 breakpoint_store,
69 worktree_store,
70 scrollbar_state,
71 breakpoints: Default::default(),
72 hide_scrollbar_task: None,
73 show_scrollbar: false,
74 workspace,
75 session,
76 focus_handle,
77 scroll_handle,
78 selected_ix: None,
79 })
80 }
81
82 fn edit_line_breakpoint(
83 &mut self,
84 path: Arc<Path>,
85 row: u32,
86 action: BreakpointEditAction,
87 cx: &mut Context<Self>,
88 ) {
89 self.breakpoint_store.update(cx, |breakpoint_store, cx| {
90 if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
91 breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
92 } else {
93 log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
94 }
95 })
96 }
97
98 fn go_to_line_breakpoint(
99 &mut self,
100 path: Arc<Path>,
101 row: u32,
102 window: &mut Window,
103 cx: &mut Context<Self>,
104 ) {
105 let task = self
106 .worktree_store
107 .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
108 cx.spawn_in(window, async move |this, cx| {
109 let (worktree, relative_path) = task.await?;
110 let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
111 let item = this
112 .update_in(cx, |this, window, cx| {
113 this.workspace.update(cx, |this, cx| {
114 this.open_path((worktree_id, relative_path), None, true, window, cx)
115 })
116 })??
117 .await?;
118 if let Some(editor) = item.downcast::<Editor>() {
119 editor
120 .update_in(cx, |this, window, cx| {
121 this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
122 })
123 .ok();
124 }
125 anyhow::Ok(())
126 })
127 .detach();
128 }
129
130 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
131 self.selected_ix = ix;
132 if let Some(ix) = ix {
133 self.scroll_handle
134 .scroll_to_item(ix, ScrollStrategy::Center);
135 }
136 cx.notify();
137 }
138
139 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
140 let ix = match self.selected_ix {
141 _ if self.breakpoints.len() == 0 => None,
142 None => Some(0),
143 Some(ix) => {
144 if ix == self.breakpoints.len() - 1 {
145 Some(0)
146 } else {
147 Some(ix + 1)
148 }
149 }
150 };
151 self.select_ix(ix, cx);
152 }
153
154 fn select_previous(
155 &mut self,
156 _: &menu::SelectPrevious,
157 _window: &mut Window,
158 cx: &mut Context<Self>,
159 ) {
160 let ix = match self.selected_ix {
161 _ if self.breakpoints.len() == 0 => None,
162 None => Some(self.breakpoints.len() - 1),
163 Some(ix) => {
164 if ix == 0 {
165 Some(self.breakpoints.len() - 1)
166 } else {
167 Some(ix - 1)
168 }
169 }
170 };
171 self.select_ix(ix, cx);
172 }
173
174 fn select_first(
175 &mut self,
176 _: &menu::SelectFirst,
177 _window: &mut Window,
178 cx: &mut Context<Self>,
179 ) {
180 let ix = if self.breakpoints.len() > 0 {
181 Some(0)
182 } else {
183 None
184 };
185 self.select_ix(ix, cx);
186 }
187
188 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
189 let ix = if self.breakpoints.len() > 0 {
190 Some(self.breakpoints.len() - 1)
191 } else {
192 None
193 };
194 self.select_ix(ix, cx);
195 }
196
197 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
198 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
199 return;
200 };
201
202 match &mut entry.kind {
203 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
204 let path = line_breakpoint.breakpoint.path.clone();
205 let row = line_breakpoint.breakpoint.row;
206 self.go_to_line_breakpoint(path, row, window, cx);
207 }
208 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
209 }
210 }
211
212 fn toggle_enable_breakpoint(
213 &mut self,
214 _: &ToggleEnableBreakpoint,
215 _window: &mut Window,
216 cx: &mut Context<Self>,
217 ) {
218 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
219 return;
220 };
221
222 match &mut entry.kind {
223 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
224 let path = line_breakpoint.breakpoint.path.clone();
225 let row = line_breakpoint.breakpoint.row;
226 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
227 }
228 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
229 if let Some(session) = &self.session {
230 let id = exception_breakpoint.id.clone();
231 session.update(cx, |session, cx| {
232 session.toggle_exception_breakpoint(&id, cx);
233 });
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 "breakpoint-list",
281 self.breakpoints.len(),
282 cx.processor(move |this, range: Range<usize>, window, cx| {
283 range
284 .clone()
285 .zip(&mut this.breakpoints[range])
286 .map(|(ix, breakpoint)| {
287 breakpoint
288 .render(ix, focus_handle.clone(), window, cx)
289 .toggle_state(Some(ix) == selected_ix)
290 .into_any_element()
291 })
292 .collect()
293 }),
294 )
295 .track_scroll(self.scroll_handle.clone())
296 .flex_grow()
297 }
298
299 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
300 if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
301 return None;
302 }
303 Some(
304 div()
305 .occlude()
306 .id("breakpoint-list-vertical-scrollbar")
307 .on_mouse_move(cx.listener(|_, _, _, cx| {
308 cx.notify();
309 cx.stop_propagation()
310 }))
311 .on_hover(|_, _, cx| {
312 cx.stop_propagation();
313 })
314 .on_any_mouse_down(|_, _, cx| {
315 cx.stop_propagation();
316 })
317 .on_mouse_up(
318 MouseButton::Left,
319 cx.listener(|_, _, _, cx| {
320 cx.stop_propagation();
321 }),
322 )
323 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
324 cx.notify();
325 }))
326 .h_full()
327 .absolute()
328 .right_1()
329 .top_1()
330 .bottom_0()
331 .w(px(12.))
332 .cursor_default()
333 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
334 )
335 }
336}
337impl Render for BreakpointList {
338 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
339 // let old_len = self.breakpoints.len();
340 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
341 self.breakpoints.clear();
342 let weak = cx.weak_entity();
343 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
344 let relative_worktree_path = self
345 .worktree_store
346 .read(cx)
347 .find_worktree(&path, cx)
348 .and_then(|(worktree, relative_path)| {
349 worktree
350 .read(cx)
351 .is_visible()
352 .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
353 });
354 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
355 let weak = weak.clone();
356 breakpoints.into_iter().filter_map(move |breakpoint| {
357 debug_assert_eq!(&path, &breakpoint.path);
358 let file_name = breakpoint.path.file_name()?;
359
360 let dir = relative_worktree_path
361 .clone()
362 .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
363 .parent()
364 .and_then(|parent| {
365 parent
366 .to_str()
367 .map(ToOwned::to_owned)
368 .map(SharedString::from)
369 });
370 let name = file_name
371 .to_str()
372 .map(ToOwned::to_owned)
373 .map(SharedString::from)?;
374 let weak = weak.clone();
375 let line = breakpoint.row + 1;
376 Some(BreakpointEntry {
377 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
378 name,
379 dir,
380 line,
381 breakpoint,
382 }),
383 weak,
384 })
385 })
386 });
387 let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
388 session
389 .read(cx)
390 .exception_breakpoints()
391 .map(|(data, is_enabled)| BreakpointEntry {
392 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
393 id: data.filter.clone(),
394 data: data.clone(),
395 is_enabled: *is_enabled,
396 }),
397 weak: weak.clone(),
398 })
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 h_flex()
510 .child(
511 IconButton::new(
512 SharedString::from(format!(
513 "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
514 self.dir, self.name, self.line
515 )),
516 IconName::Close,
517 )
518 .on_click({
519 let weak = weak.clone();
520 let path = path.clone();
521 move |_, _, cx| {
522 weak.update(cx, |breakpoint_list, cx| {
523 breakpoint_list.edit_line_breakpoint(
524 path.clone(),
525 row,
526 BreakpointEditAction::Toggle,
527 cx,
528 );
529 })
530 .ok();
531 }
532 })
533 .tooltip(move |window, cx| {
534 Tooltip::for_action_in(
535 "Unset Breakpoint",
536 &UnsetBreakpoint,
537 &focus_handle,
538 window,
539 cx,
540 )
541 })
542 .icon_size(ui::IconSize::XSmall),
543 )
544 .right_4(),
545 )
546 .child(
547 v_flex()
548 .py_1()
549 .gap_1()
550 .min_h(px(26.))
551 .justify_center()
552 .id(SharedString::from(format!(
553 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
554 self.dir, self.name, self.line
555 )))
556 .on_click(move |_, window, cx| {
557 weak.update(cx, |breakpoint_list, cx| {
558 breakpoint_list.select_ix(Some(ix), cx);
559 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
560 })
561 .ok();
562 })
563 .cursor_pointer()
564 .child(
565 h_flex()
566 .gap_1()
567 .child(
568 Label::new(format!("{}:{}", self.name, self.line))
569 .size(LabelSize::Small)
570 .line_height_style(ui::LineHeightStyle::UiLabel),
571 )
572 .children(self.dir.clone().map(|dir| {
573 Label::new(dir)
574 .color(Color::Muted)
575 .size(LabelSize::Small)
576 .line_height_style(ui::LineHeightStyle::UiLabel)
577 })),
578 ),
579 )
580 }
581}
582#[derive(Clone, Debug)]
583struct ExceptionBreakpoint {
584 id: String,
585 data: ExceptionBreakpointsFilter,
586 is_enabled: bool,
587}
588
589impl ExceptionBreakpoint {
590 fn render(
591 &mut self,
592 ix: usize,
593 focus_handle: FocusHandle,
594 list: WeakEntity<BreakpointList>,
595 ) -> ListItem {
596 let color = if self.is_enabled {
597 Color::Debugger
598 } else {
599 Color::Muted
600 };
601 let id = SharedString::from(&self.id);
602 let is_enabled = self.is_enabled;
603
604 ListItem::new(SharedString::from(format!(
605 "exception-breakpoint-ui-item-{}",
606 self.id
607 )))
608 .on_click({
609 let list = list.clone();
610 move |_, _, cx| {
611 list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
612 .ok();
613 }
614 })
615 .rounded()
616 .on_secondary_mouse_down(|_, _, cx| {
617 cx.stop_propagation();
618 })
619 .start_slot(
620 div()
621 .id(SharedString::from(format!(
622 "exception-breakpoint-ui-item-{}-click-handler",
623 self.id
624 )))
625 .tooltip(move |window, cx| {
626 Tooltip::for_action_in(
627 if is_enabled {
628 "Disable Exception Breakpoint"
629 } else {
630 "Enable Exception Breakpoint"
631 },
632 &ToggleEnableBreakpoint,
633 &focus_handle,
634 window,
635 cx,
636 )
637 })
638 .on_click({
639 let list = list.clone();
640 move |_, _, cx| {
641 list.update(cx, |this, cx| {
642 if let Some(session) = &this.session {
643 session.update(cx, |this, cx| {
644 this.toggle_exception_breakpoint(&id, cx);
645 });
646 cx.notify();
647 }
648 })
649 .ok();
650 }
651 })
652 .cursor_pointer()
653 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
654 )
655 .child(
656 v_flex()
657 .py_1()
658 .gap_1()
659 .min_h(px(26.))
660 .justify_center()
661 .id(("exception-breakpoint-label", ix))
662 .child(
663 Label::new(self.data.label.clone())
664 .size(LabelSize::Small)
665 .line_height_style(ui::LineHeightStyle::UiLabel),
666 )
667 .when_some(self.data.description.clone(), |el, description| {
668 el.tooltip(Tooltip::text(description))
669 }),
670 )
671 }
672}
673#[derive(Clone, Debug)]
674enum BreakpointEntryKind {
675 LineBreakpoint(LineBreakpoint),
676 ExceptionBreakpoint(ExceptionBreakpoint),
677}
678
679#[derive(Clone, Debug)]
680struct BreakpointEntry {
681 kind: BreakpointEntryKind,
682 weak: WeakEntity<BreakpointList>,
683}
684
685impl BreakpointEntry {
686 fn render(
687 &mut self,
688 ix: usize,
689 focus_handle: FocusHandle,
690 _: &mut Window,
691 _: &mut App,
692 ) -> ListItem {
693 match &mut self.kind {
694 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
695 line_breakpoint.render(ix, focus_handle, self.weak.clone())
696 }
697 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
698 exception_breakpoint.render(ix, focus_handle, self.weak.clone())
699 }
700 }
701 }
702}