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 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 this.session.update(cx, |this, cx| {
643 this.toggle_exception_breakpoint(&id, cx);
644 });
645 cx.notify();
646 })
647 .ok();
648 }
649 })
650 .cursor_pointer()
651 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
652 )
653 .child(
654 v_flex()
655 .py_1()
656 .gap_1()
657 .min_h(px(26.))
658 .justify_center()
659 .id(("exception-breakpoint-label", ix))
660 .child(
661 Label::new(self.data.label.clone())
662 .size(LabelSize::Small)
663 .line_height_style(ui::LineHeightStyle::UiLabel),
664 )
665 .when_some(self.data.description.clone(), |el, description| {
666 el.tooltip(Tooltip::text(description))
667 }),
668 )
669 }
670}
671#[derive(Clone, Debug)]
672enum BreakpointEntryKind {
673 LineBreakpoint(LineBreakpoint),
674 ExceptionBreakpoint(ExceptionBreakpoint),
675}
676
677#[derive(Clone, Debug)]
678struct BreakpointEntry {
679 kind: BreakpointEntryKind,
680 weak: WeakEntity<BreakpointList>,
681}
682
683impl BreakpointEntry {
684 fn render(
685 &mut self,
686 ix: usize,
687 focus_handle: FocusHandle,
688 _: &mut Window,
689 _: &mut App,
690 ) -> ListItem {
691 match &mut self.kind {
692 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
693 line_breakpoint.render(ix, focus_handle, self.weak.clone())
694 }
695 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
696 exception_breakpoint.render(ix, focus_handle, self.weak.clone())
697 }
698 }
699 }
700}