1use std::{
2 ops::Range,
3 path::{Path, PathBuf},
4 sync::Arc,
5 time::Duration,
6};
7
8use dap::{Capabilities, ExceptionBreakpointsFilter};
9use editor::Editor;
10use gpui::{
11 Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
12 Stateful, Task, UniformListScrollHandle, WeakEntity, actions, 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 ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
25 Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
26 InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
27 Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
28 Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
29};
30use util::ResultExt;
31use workspace::Workspace;
32use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
33
34actions!(
35 debugger,
36 [
37 /// Navigates to the previous breakpoint property in the list.
38 PreviousBreakpointProperty,
39 /// Navigates to the next breakpoint property in the list.
40 NextBreakpointProperty
41 ]
42);
43#[derive(Clone, Copy, PartialEq)]
44pub(crate) enum SelectedBreakpointKind {
45 Source,
46 Exception,
47}
48pub(crate) struct BreakpointList {
49 workspace: WeakEntity<Workspace>,
50 breakpoint_store: Entity<BreakpointStore>,
51 worktree_store: Entity<WorktreeStore>,
52 scrollbar_state: ScrollbarState,
53 breakpoints: Vec<BreakpointEntry>,
54 session: Option<Entity<Session>>,
55 hide_scrollbar_task: Option<Task<()>>,
56 show_scrollbar: bool,
57 focus_handle: FocusHandle,
58 scroll_handle: UniformListScrollHandle,
59 selected_ix: Option<usize>,
60 input: Entity<Editor>,
61 strip_mode: Option<ActiveBreakpointStripMode>,
62}
63
64impl Focusable for BreakpointList {
65 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
66 self.focus_handle.clone()
67 }
68}
69
70#[derive(Clone, Copy, PartialEq)]
71enum ActiveBreakpointStripMode {
72 Log,
73 Condition,
74 HitCondition,
75}
76
77impl BreakpointList {
78 pub(crate) fn new(
79 session: Option<Entity<Session>>,
80 workspace: WeakEntity<Workspace>,
81 project: &Entity<Project>,
82 window: &mut Window,
83 cx: &mut App,
84 ) -> Entity<Self> {
85 let project = project.read(cx);
86 let breakpoint_store = project.breakpoint_store();
87 let worktree_store = project.worktree_store();
88 let focus_handle = cx.focus_handle();
89 let scroll_handle = UniformListScrollHandle::new();
90 let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
91
92 cx.new(|cx| Self {
93 breakpoint_store,
94 worktree_store,
95 scrollbar_state,
96 breakpoints: Default::default(),
97 hide_scrollbar_task: None,
98 show_scrollbar: false,
99 workspace,
100 session,
101 focus_handle,
102 scroll_handle,
103 selected_ix: None,
104 input: cx.new(|cx| Editor::single_line(window, cx)),
105 strip_mode: None,
106 })
107 }
108
109 fn edit_line_breakpoint(
110 &self,
111 path: Arc<Path>,
112 row: u32,
113 action: BreakpointEditAction,
114 cx: &mut App,
115 ) {
116 Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
117 }
118 fn edit_line_breakpoint_inner(
119 breakpoint_store: &Entity<BreakpointStore>,
120 path: Arc<Path>,
121 row: u32,
122 action: BreakpointEditAction,
123 cx: &mut App,
124 ) {
125 breakpoint_store.update(cx, |breakpoint_store, cx| {
126 if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
127 breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
128 } else {
129 log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
130 }
131 })
132 }
133
134 fn go_to_line_breakpoint(
135 &mut self,
136 path: Arc<Path>,
137 row: u32,
138 window: &mut Window,
139 cx: &mut Context<Self>,
140 ) {
141 let task = self
142 .worktree_store
143 .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
144 cx.spawn_in(window, async move |this, cx| {
145 let (worktree, relative_path) = task.await?;
146 let worktree_id = worktree.read_with(cx, |this, _| this.id())?;
147 let item = this
148 .update_in(cx, |this, window, cx| {
149 this.workspace.update(cx, |this, cx| {
150 this.open_path((worktree_id, relative_path), None, true, window, cx)
151 })
152 })??
153 .await?;
154 if let Some(editor) = item.downcast::<Editor>() {
155 editor
156 .update_in(cx, |this, window, cx| {
157 this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
158 })
159 .ok();
160 }
161 anyhow::Ok(())
162 })
163 .detach();
164 }
165
166 pub(crate) fn selection_kind(&self) -> Option<(SelectedBreakpointKind, bool)> {
167 self.selected_ix.and_then(|ix| {
168 self.breakpoints.get(ix).map(|bp| match &bp.kind {
169 BreakpointEntryKind::LineBreakpoint(bp) => (
170 SelectedBreakpointKind::Source,
171 bp.breakpoint.state
172 == project::debugger::breakpoint_store::BreakpointState::Enabled,
173 ),
174 BreakpointEntryKind::ExceptionBreakpoint(bp) => {
175 (SelectedBreakpointKind::Exception, bp.is_enabled)
176 }
177 })
178 })
179 }
180
181 fn set_active_breakpoint_property(
182 &mut self,
183 prop: ActiveBreakpointStripMode,
184 window: &mut Window,
185 cx: &mut App,
186 ) {
187 self.strip_mode = Some(prop);
188 let placeholder = match prop {
189 ActiveBreakpointStripMode::Log => "Set Log Message",
190 ActiveBreakpointStripMode::Condition => "Set Condition",
191 ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
192 };
193 let mut is_exception_breakpoint = true;
194 let active_value = self.selected_ix.and_then(|ix| {
195 self.breakpoints.get(ix).and_then(|bp| {
196 if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
197 is_exception_breakpoint = false;
198 match prop {
199 ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
200 ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
201 ActiveBreakpointStripMode::HitCondition => {
202 bp.breakpoint.hit_condition.clone()
203 }
204 }
205 } else {
206 None
207 }
208 })
209 });
210
211 self.input.update(cx, |this, cx| {
212 this.set_placeholder_text(placeholder, cx);
213 this.set_read_only(is_exception_breakpoint);
214 this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
215 });
216 }
217
218 fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
219 self.selected_ix = ix;
220 if let Some(ix) = ix {
221 self.scroll_handle
222 .scroll_to_item(ix, ScrollStrategy::Center);
223 }
224 if let Some(mode) = self.strip_mode {
225 self.set_active_breakpoint_property(mode, window, cx);
226 }
227
228 cx.notify();
229 }
230
231 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
232 if self.strip_mode.is_some() {
233 if self.input.focus_handle(cx).contains_focused(window, cx) {
234 cx.propagate();
235 return;
236 }
237 }
238 let ix = match self.selected_ix {
239 _ if self.breakpoints.len() == 0 => None,
240 None => Some(0),
241 Some(ix) => {
242 if ix == self.breakpoints.len() - 1 {
243 Some(0)
244 } else {
245 Some(ix + 1)
246 }
247 }
248 };
249 self.select_ix(ix, window, cx);
250 }
251
252 fn select_previous(
253 &mut self,
254 _: &menu::SelectPrevious,
255 window: &mut Window,
256 cx: &mut Context<Self>,
257 ) {
258 if self.strip_mode.is_some() {
259 if self.input.focus_handle(cx).contains_focused(window, cx) {
260 cx.propagate();
261 return;
262 }
263 }
264 let ix = match self.selected_ix {
265 _ if self.breakpoints.len() == 0 => None,
266 None => Some(self.breakpoints.len() - 1),
267 Some(ix) => {
268 if ix == 0 {
269 Some(self.breakpoints.len() - 1)
270 } else {
271 Some(ix - 1)
272 }
273 }
274 };
275 self.select_ix(ix, window, cx);
276 }
277
278 fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
279 if self.strip_mode.is_some() {
280 if self.input.focus_handle(cx).contains_focused(window, cx) {
281 cx.propagate();
282 return;
283 }
284 }
285 let ix = if self.breakpoints.len() > 0 {
286 Some(0)
287 } else {
288 None
289 };
290 self.select_ix(ix, window, cx);
291 }
292
293 fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
294 if self.strip_mode.is_some() {
295 if self.input.focus_handle(cx).contains_focused(window, cx) {
296 cx.propagate();
297 return;
298 }
299 }
300 let ix = if self.breakpoints.len() > 0 {
301 Some(self.breakpoints.len() - 1)
302 } else {
303 None
304 };
305 self.select_ix(ix, window, cx);
306 }
307
308 fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
309 if self.input.focus_handle(cx).contains_focused(window, cx) {
310 self.focus_handle.focus(window);
311 } else if self.strip_mode.is_some() {
312 self.strip_mode.take();
313 cx.notify();
314 } else {
315 cx.propagate();
316 }
317 }
318 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
319 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
320 return;
321 };
322
323 if let Some(mode) = self.strip_mode {
324 let handle = self.input.focus_handle(cx);
325 if handle.is_focused(window) {
326 // Go back to the main strip. Save the result as well.
327 let text = self.input.read(cx).text(cx);
328
329 match mode {
330 ActiveBreakpointStripMode::Log => match &entry.kind {
331 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
332 Self::edit_line_breakpoint_inner(
333 &self.breakpoint_store,
334 line_breakpoint.breakpoint.path.clone(),
335 line_breakpoint.breakpoint.row,
336 BreakpointEditAction::EditLogMessage(Arc::from(text)),
337 cx,
338 );
339 }
340 _ => {}
341 },
342 ActiveBreakpointStripMode::Condition => match &entry.kind {
343 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
344 Self::edit_line_breakpoint_inner(
345 &self.breakpoint_store,
346 line_breakpoint.breakpoint.path.clone(),
347 line_breakpoint.breakpoint.row,
348 BreakpointEditAction::EditCondition(Arc::from(text)),
349 cx,
350 );
351 }
352 _ => {}
353 },
354 ActiveBreakpointStripMode::HitCondition => match &entry.kind {
355 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
356 Self::edit_line_breakpoint_inner(
357 &self.breakpoint_store,
358 line_breakpoint.breakpoint.path.clone(),
359 line_breakpoint.breakpoint.row,
360 BreakpointEditAction::EditHitCondition(Arc::from(text)),
361 cx,
362 );
363 }
364 _ => {}
365 },
366 }
367 self.focus_handle.focus(window);
368 } else {
369 handle.focus(window);
370 }
371
372 return;
373 }
374 match &mut entry.kind {
375 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
376 let path = line_breakpoint.breakpoint.path.clone();
377 let row = line_breakpoint.breakpoint.row;
378 self.go_to_line_breakpoint(path, row, window, cx);
379 }
380 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
381 }
382 }
383
384 fn toggle_enable_breakpoint(
385 &mut self,
386 _: &ToggleEnableBreakpoint,
387 window: &mut Window,
388 cx: &mut Context<Self>,
389 ) {
390 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
391 return;
392 };
393 if self.strip_mode.is_some() {
394 if self.input.focus_handle(cx).contains_focused(window, cx) {
395 cx.propagate();
396 return;
397 }
398 }
399
400 match &mut entry.kind {
401 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
402 let path = line_breakpoint.breakpoint.path.clone();
403 let row = line_breakpoint.breakpoint.row;
404 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
405 }
406 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
407 if let Some(session) = &self.session {
408 let id = exception_breakpoint.id.clone();
409 session.update(cx, |session, cx| {
410 session.toggle_exception_breakpoint(&id, cx);
411 });
412 }
413 }
414 }
415 cx.notify();
416 }
417
418 fn unset_breakpoint(
419 &mut self,
420 _: &UnsetBreakpoint,
421 _window: &mut Window,
422 cx: &mut Context<Self>,
423 ) {
424 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
425 return;
426 };
427
428 match &mut entry.kind {
429 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
430 let path = line_breakpoint.breakpoint.path.clone();
431 let row = line_breakpoint.breakpoint.row;
432 self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
433 }
434 BreakpointEntryKind::ExceptionBreakpoint(_) => {}
435 }
436 cx.notify();
437 }
438
439 fn previous_breakpoint_property(
440 &mut self,
441 _: &PreviousBreakpointProperty,
442 window: &mut Window,
443 cx: &mut Context<Self>,
444 ) {
445 let next_mode = match self.strip_mode {
446 Some(ActiveBreakpointStripMode::Log) => None,
447 Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
448 Some(ActiveBreakpointStripMode::HitCondition) => {
449 Some(ActiveBreakpointStripMode::Condition)
450 }
451 None => Some(ActiveBreakpointStripMode::HitCondition),
452 };
453 if let Some(mode) = next_mode {
454 self.set_active_breakpoint_property(mode, window, cx);
455 } else {
456 self.strip_mode.take();
457 }
458
459 cx.notify();
460 }
461 fn next_breakpoint_property(
462 &mut self,
463 _: &NextBreakpointProperty,
464 window: &mut Window,
465 cx: &mut Context<Self>,
466 ) {
467 let next_mode = match self.strip_mode {
468 Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
469 Some(ActiveBreakpointStripMode::Condition) => {
470 Some(ActiveBreakpointStripMode::HitCondition)
471 }
472 Some(ActiveBreakpointStripMode::HitCondition) => None,
473 None => Some(ActiveBreakpointStripMode::Log),
474 };
475 if let Some(mode) = next_mode {
476 self.set_active_breakpoint_property(mode, window, cx);
477 } else {
478 self.strip_mode.take();
479 }
480 cx.notify();
481 }
482
483 fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
484 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
485 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
486 cx.background_executor()
487 .timer(SCROLLBAR_SHOW_INTERVAL)
488 .await;
489 panel
490 .update(cx, |panel, cx| {
491 panel.show_scrollbar = false;
492 cx.notify();
493 })
494 .log_err();
495 }))
496 }
497
498 fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
499 let selected_ix = self.selected_ix;
500 let focus_handle = self.focus_handle.clone();
501 let supported_breakpoint_properties = self
502 .session
503 .as_ref()
504 .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
505 .unwrap_or_else(SupportedBreakpointProperties::empty);
506 let strip_mode = self.strip_mode;
507 uniform_list(
508 "breakpoint-list",
509 self.breakpoints.len(),
510 cx.processor(move |this, range: Range<usize>, _, _| {
511 range
512 .clone()
513 .zip(&mut this.breakpoints[range])
514 .map(|(ix, breakpoint)| {
515 breakpoint
516 .render(
517 strip_mode,
518 supported_breakpoint_properties,
519 ix,
520 Some(ix) == selected_ix,
521 focus_handle.clone(),
522 )
523 .into_any_element()
524 })
525 .collect()
526 }),
527 )
528 .track_scroll(self.scroll_handle.clone())
529 .flex_grow()
530 }
531
532 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
533 if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
534 return None;
535 }
536 Some(
537 div()
538 .occlude()
539 .id("breakpoint-list-vertical-scrollbar")
540 .on_mouse_move(cx.listener(|_, _, _, cx| {
541 cx.notify();
542 cx.stop_propagation()
543 }))
544 .on_hover(|_, _, cx| {
545 cx.stop_propagation();
546 })
547 .on_any_mouse_down(|_, _, cx| {
548 cx.stop_propagation();
549 })
550 .on_mouse_up(
551 MouseButton::Left,
552 cx.listener(|_, _, _, cx| {
553 cx.stop_propagation();
554 }),
555 )
556 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
557 cx.notify();
558 }))
559 .h_full()
560 .absolute()
561 .right_1()
562 .top_1()
563 .bottom_0()
564 .w(px(12.))
565 .cursor_default()
566 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
567 )
568 }
569 pub(crate) fn render_control_strip(&self) -> AnyElement {
570 let selection_kind = self.selection_kind();
571 let focus_handle = self.focus_handle.clone();
572 let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
573 SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
574 SelectedBreakpointKind::Exception => {
575 "Exception Breakpoints cannot be removed from the breakpoint list"
576 }
577 });
578 let toggle_label = selection_kind.map(|(_, is_enabled)| {
579 if is_enabled {
580 (
581 "Disable Breakpoint",
582 "Disable a breakpoint without removing it from the list",
583 )
584 } else {
585 ("Enable Breakpoint", "Re-enable a breakpoint")
586 }
587 });
588
589 h_flex()
590 .gap_2()
591 .child(
592 IconButton::new(
593 "disable-breakpoint-breakpoint-list",
594 IconName::DebugDisabledBreakpoint,
595 )
596 .icon_size(IconSize::XSmall)
597 .when_some(toggle_label, |this, (label, meta)| {
598 this.tooltip({
599 let focus_handle = focus_handle.clone();
600 move |window, cx| {
601 Tooltip::with_meta_in(
602 label,
603 Some(&ToggleEnableBreakpoint),
604 meta,
605 &focus_handle,
606 window,
607 cx,
608 )
609 }
610 })
611 })
612 .disabled(selection_kind.is_none())
613 .on_click({
614 let focus_handle = focus_handle.clone();
615 move |_, window, cx| {
616 focus_handle.focus(window);
617 window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
618 }
619 }),
620 )
621 .child(
622 IconButton::new("remove-breakpoint-breakpoint-list", IconName::X)
623 .icon_size(IconSize::XSmall)
624 .icon_color(ui::Color::Error)
625 .when_some(remove_breakpoint_tooltip, |this, tooltip| {
626 this.tooltip({
627 let focus_handle = focus_handle.clone();
628 move |window, cx| {
629 Tooltip::with_meta_in(
630 "Remove Breakpoint",
631 Some(&UnsetBreakpoint),
632 tooltip,
633 &focus_handle,
634 window,
635 cx,
636 )
637 }
638 })
639 })
640 .disabled(
641 selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
642 )
643 .on_click({
644 let focus_handle = focus_handle.clone();
645 move |_, window, cx| {
646 focus_handle.focus(window);
647 window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
648 }
649 }),
650 )
651 .mr_2()
652 .into_any_element()
653 }
654}
655
656impl Render for BreakpointList {
657 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
658 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
659 self.breakpoints.clear();
660 let weak = cx.weak_entity();
661 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
662 let relative_worktree_path = self
663 .worktree_store
664 .read(cx)
665 .find_worktree(&path, cx)
666 .and_then(|(worktree, relative_path)| {
667 worktree
668 .read(cx)
669 .is_visible()
670 .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
671 });
672 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
673 let weak = weak.clone();
674 breakpoints.into_iter().filter_map(move |breakpoint| {
675 debug_assert_eq!(&path, &breakpoint.path);
676 let file_name = breakpoint.path.file_name()?;
677
678 let dir = relative_worktree_path
679 .clone()
680 .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
681 .parent()
682 .and_then(|parent| {
683 parent
684 .to_str()
685 .map(ToOwned::to_owned)
686 .map(SharedString::from)
687 });
688 let name = file_name
689 .to_str()
690 .map(ToOwned::to_owned)
691 .map(SharedString::from)?;
692 let weak = weak.clone();
693 let line = breakpoint.row + 1;
694 Some(BreakpointEntry {
695 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
696 name,
697 dir,
698 line,
699 breakpoint,
700 }),
701 weak,
702 })
703 })
704 });
705 let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
706 session
707 .read(cx)
708 .exception_breakpoints()
709 .map(|(data, is_enabled)| BreakpointEntry {
710 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
711 id: data.filter.clone(),
712 data: data.clone(),
713 is_enabled: *is_enabled,
714 }),
715 weak: weak.clone(),
716 })
717 });
718 self.breakpoints
719 .extend(breakpoints.chain(exception_breakpoints));
720 v_flex()
721 .id("breakpoint-list")
722 .key_context("BreakpointList")
723 .track_focus(&self.focus_handle)
724 .on_hover(cx.listener(|this, hovered, window, cx| {
725 if *hovered {
726 this.show_scrollbar = true;
727 this.hide_scrollbar_task.take();
728 cx.notify();
729 } else if !this.focus_handle.contains_focused(window, cx) {
730 this.hide_scrollbar(window, cx);
731 }
732 }))
733 .on_action(cx.listener(Self::select_next))
734 .on_action(cx.listener(Self::select_previous))
735 .on_action(cx.listener(Self::select_first))
736 .on_action(cx.listener(Self::select_last))
737 .on_action(cx.listener(Self::dismiss))
738 .on_action(cx.listener(Self::confirm))
739 .on_action(cx.listener(Self::toggle_enable_breakpoint))
740 .on_action(cx.listener(Self::unset_breakpoint))
741 .on_action(cx.listener(Self::next_breakpoint_property))
742 .on_action(cx.listener(Self::previous_breakpoint_property))
743 .size_full()
744 .m_0p5()
745 .child(
746 v_flex()
747 .size_full()
748 .child(self.render_list(cx))
749 .children(self.render_vertical_scrollbar(cx)),
750 )
751 .when_some(self.strip_mode, |this, _| {
752 this.child(Divider::horizontal()).child(
753 h_flex()
754 // .w_full()
755 .m_0p5()
756 .p_0p5()
757 .border_1()
758 .rounded_sm()
759 .when(
760 self.input.focus_handle(cx).contains_focused(window, cx),
761 |this| {
762 let colors = cx.theme().colors();
763 let border = if self.input.read(cx).read_only(cx) {
764 colors.border_disabled
765 } else {
766 colors.border_focused
767 };
768 this.border_color(border)
769 },
770 )
771 .child(self.input.clone()),
772 )
773 })
774 }
775}
776
777#[derive(Clone, Debug)]
778struct LineBreakpoint {
779 name: SharedString,
780 dir: Option<SharedString>,
781 line: u32,
782 breakpoint: SourceBreakpoint,
783}
784
785impl LineBreakpoint {
786 fn render(
787 &mut self,
788 props: SupportedBreakpointProperties,
789 strip_mode: Option<ActiveBreakpointStripMode>,
790 ix: usize,
791 is_selected: bool,
792 focus_handle: FocusHandle,
793 weak: WeakEntity<BreakpointList>,
794 ) -> ListItem {
795 let icon_name = if self.breakpoint.state.is_enabled() {
796 IconName::DebugBreakpoint
797 } else {
798 IconName::DebugDisabledBreakpoint
799 };
800 let path = self.breakpoint.path.clone();
801 let row = self.breakpoint.row;
802 let is_enabled = self.breakpoint.state.is_enabled();
803 let indicator = div()
804 .id(SharedString::from(format!(
805 "breakpoint-ui-toggle-{:?}/{}:{}",
806 self.dir, self.name, self.line
807 )))
808 .cursor_pointer()
809 .tooltip({
810 let focus_handle = focus_handle.clone();
811 move |window, cx| {
812 Tooltip::for_action_in(
813 if is_enabled {
814 "Disable Breakpoint"
815 } else {
816 "Enable Breakpoint"
817 },
818 &ToggleEnableBreakpoint,
819 &focus_handle,
820 window,
821 cx,
822 )
823 }
824 })
825 .on_click({
826 let weak = weak.clone();
827 let path = path.clone();
828 move |_, _, cx| {
829 weak.update(cx, |breakpoint_list, cx| {
830 breakpoint_list.edit_line_breakpoint(
831 path.clone(),
832 row,
833 BreakpointEditAction::InvertState,
834 cx,
835 );
836 })
837 .ok();
838 }
839 })
840 .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
841 .on_mouse_down(MouseButton::Left, move |_, _, _| {});
842
843 ListItem::new(SharedString::from(format!(
844 "breakpoint-ui-item-{:?}/{}:{}",
845 self.dir, self.name, self.line
846 )))
847 .on_click({
848 let weak = weak.clone();
849 move |_, window, cx| {
850 weak.update(cx, |breakpoint_list, cx| {
851 breakpoint_list.select_ix(Some(ix), window, cx);
852 })
853 .ok();
854 }
855 })
856 .start_slot(indicator)
857 .rounded()
858 .on_secondary_mouse_down(|_, _, cx| {
859 cx.stop_propagation();
860 })
861 .child(
862 h_flex()
863 .w_full()
864 .mr_4()
865 .py_0p5()
866 .gap_1()
867 .min_h(px(26.))
868 .justify_between()
869 .id(SharedString::from(format!(
870 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
871 self.dir, self.name, self.line
872 )))
873 .on_click({
874 let weak = weak.clone();
875 move |_, window, cx| {
876 weak.update(cx, |breakpoint_list, cx| {
877 breakpoint_list.select_ix(Some(ix), window, cx);
878 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
879 })
880 .ok();
881 }
882 })
883 .cursor_pointer()
884 .child(
885 h_flex()
886 .gap_0p5()
887 .child(
888 Label::new(format!("{}:{}", self.name, self.line))
889 .size(LabelSize::Small)
890 .line_height_style(ui::LineHeightStyle::UiLabel),
891 )
892 .children(self.dir.as_ref().and_then(|dir| {
893 let path_without_root = Path::new(dir.as_ref())
894 .components()
895 .skip(1)
896 .collect::<PathBuf>();
897 path_without_root.components().next()?;
898 Some(
899 Label::new(path_without_root.to_string_lossy().into_owned())
900 .color(Color::Muted)
901 .size(LabelSize::Small)
902 .line_height_style(ui::LineHeightStyle::UiLabel)
903 .truncate(),
904 )
905 })),
906 )
907 .when_some(self.dir.as_ref(), |this, parent_dir| {
908 this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
909 })
910 .child(BreakpointOptionsStrip {
911 props,
912 breakpoint: BreakpointEntry {
913 kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
914 weak: weak,
915 },
916 is_selected,
917 focus_handle,
918 strip_mode,
919 index: ix,
920 }),
921 )
922 .toggle_state(is_selected)
923 }
924}
925#[derive(Clone, Debug)]
926struct ExceptionBreakpoint {
927 id: String,
928 data: ExceptionBreakpointsFilter,
929 is_enabled: bool,
930}
931
932impl ExceptionBreakpoint {
933 fn render(
934 &mut self,
935 props: SupportedBreakpointProperties,
936 strip_mode: Option<ActiveBreakpointStripMode>,
937 ix: usize,
938 is_selected: bool,
939 focus_handle: FocusHandle,
940 list: WeakEntity<BreakpointList>,
941 ) -> ListItem {
942 let color = if self.is_enabled {
943 Color::Debugger
944 } else {
945 Color::Muted
946 };
947 let id = SharedString::from(&self.id);
948 let is_enabled = self.is_enabled;
949 let weak = list.clone();
950 ListItem::new(SharedString::from(format!(
951 "exception-breakpoint-ui-item-{}",
952 self.id
953 )))
954 .on_click({
955 let list = list.clone();
956 move |_, window, cx| {
957 list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
958 .ok();
959 }
960 })
961 .rounded()
962 .on_secondary_mouse_down(|_, _, cx| {
963 cx.stop_propagation();
964 })
965 .start_slot(
966 div()
967 .id(SharedString::from(format!(
968 "exception-breakpoint-ui-item-{}-click-handler",
969 self.id
970 )))
971 .tooltip({
972 let focus_handle = focus_handle.clone();
973 move |window, cx| {
974 Tooltip::for_action_in(
975 if is_enabled {
976 "Disable Exception Breakpoint"
977 } else {
978 "Enable Exception Breakpoint"
979 },
980 &ToggleEnableBreakpoint,
981 &focus_handle,
982 window,
983 cx,
984 )
985 }
986 })
987 .on_click({
988 let list = list.clone();
989 move |_, _, cx| {
990 list.update(cx, |this, cx| {
991 if let Some(session) = &this.session {
992 session.update(cx, |this, cx| {
993 this.toggle_exception_breakpoint(&id, cx);
994 });
995 cx.notify();
996 }
997 })
998 .ok();
999 }
1000 })
1001 .cursor_pointer()
1002 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
1003 )
1004 .child(
1005 h_flex()
1006 .w_full()
1007 .mr_4()
1008 .py_0p5()
1009 .justify_between()
1010 .child(
1011 v_flex()
1012 .py_1()
1013 .gap_1()
1014 .min_h(px(26.))
1015 .justify_center()
1016 .id(("exception-breakpoint-label", ix))
1017 .child(
1018 Label::new(self.data.label.clone())
1019 .size(LabelSize::Small)
1020 .line_height_style(ui::LineHeightStyle::UiLabel),
1021 )
1022 .when_some(self.data.description.clone(), |el, description| {
1023 el.tooltip(Tooltip::text(description))
1024 }),
1025 )
1026 .child(BreakpointOptionsStrip {
1027 props,
1028 breakpoint: BreakpointEntry {
1029 kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1030 weak: weak,
1031 },
1032 is_selected,
1033 focus_handle,
1034 strip_mode,
1035 index: ix,
1036 }),
1037 )
1038 .toggle_state(is_selected)
1039 }
1040}
1041#[derive(Clone, Debug)]
1042enum BreakpointEntryKind {
1043 LineBreakpoint(LineBreakpoint),
1044 ExceptionBreakpoint(ExceptionBreakpoint),
1045}
1046
1047#[derive(Clone, Debug)]
1048struct BreakpointEntry {
1049 kind: BreakpointEntryKind,
1050 weak: WeakEntity<BreakpointList>,
1051}
1052
1053impl BreakpointEntry {
1054 fn render(
1055 &mut self,
1056 strip_mode: Option<ActiveBreakpointStripMode>,
1057 props: SupportedBreakpointProperties,
1058 ix: usize,
1059 is_selected: bool,
1060 focus_handle: FocusHandle,
1061 ) -> ListItem {
1062 match &mut self.kind {
1063 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1064 props,
1065 strip_mode,
1066 ix,
1067 is_selected,
1068 focus_handle,
1069 self.weak.clone(),
1070 ),
1071 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1072 .render(
1073 props.for_exception_breakpoints(),
1074 strip_mode,
1075 ix,
1076 is_selected,
1077 focus_handle,
1078 self.weak.clone(),
1079 ),
1080 }
1081 }
1082
1083 fn id(&self) -> SharedString {
1084 match &self.kind {
1085 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1086 "source-breakpoint-control-strip-{:?}:{}",
1087 line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1088 )
1089 .into(),
1090 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1091 "exception-breakpoint-control-strip--{}",
1092 exception_breakpoint.id
1093 )
1094 .into(),
1095 }
1096 }
1097
1098 fn has_log(&self) -> bool {
1099 match &self.kind {
1100 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1101 line_breakpoint.breakpoint.message.is_some()
1102 }
1103 _ => false,
1104 }
1105 }
1106
1107 fn has_condition(&self) -> bool {
1108 match &self.kind {
1109 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1110 line_breakpoint.breakpoint.condition.is_some()
1111 }
1112 // We don't support conditions on exception breakpoints
1113 BreakpointEntryKind::ExceptionBreakpoint(_) => false,
1114 }
1115 }
1116
1117 fn has_hit_condition(&self) -> bool {
1118 match &self.kind {
1119 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1120 line_breakpoint.breakpoint.hit_condition.is_some()
1121 }
1122 _ => false,
1123 }
1124 }
1125}
1126bitflags::bitflags! {
1127 #[derive(Clone, Copy)]
1128 pub struct SupportedBreakpointProperties: u32 {
1129 const LOG = 1 << 0;
1130 const CONDITION = 1 << 1;
1131 const HIT_CONDITION = 1 << 2;
1132 // Conditions for exceptions can be set only when exception filters are supported.
1133 const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1134 }
1135}
1136
1137impl From<&Capabilities> for SupportedBreakpointProperties {
1138 fn from(caps: &Capabilities) -> Self {
1139 let mut this = Self::empty();
1140 for (prop, offset) in [
1141 (caps.supports_log_points, Self::LOG),
1142 (caps.supports_conditional_breakpoints, Self::CONDITION),
1143 (
1144 caps.supports_hit_conditional_breakpoints,
1145 Self::HIT_CONDITION,
1146 ),
1147 (
1148 caps.supports_exception_options,
1149 Self::EXCEPTION_FILTER_OPTIONS,
1150 ),
1151 ] {
1152 if prop.unwrap_or_default() {
1153 this.insert(offset);
1154 }
1155 }
1156 this
1157 }
1158}
1159
1160impl SupportedBreakpointProperties {
1161 fn for_exception_breakpoints(self) -> Self {
1162 // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1163 Self::empty()
1164 }
1165}
1166#[derive(IntoElement)]
1167struct BreakpointOptionsStrip {
1168 props: SupportedBreakpointProperties,
1169 breakpoint: BreakpointEntry,
1170 is_selected: bool,
1171 focus_handle: FocusHandle,
1172 strip_mode: Option<ActiveBreakpointStripMode>,
1173 index: usize,
1174}
1175
1176impl BreakpointOptionsStrip {
1177 fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1178 self.is_selected && self.strip_mode == Some(expected_mode)
1179 }
1180 fn on_click_callback(
1181 &self,
1182 mode: ActiveBreakpointStripMode,
1183 ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1184 let list = self.breakpoint.weak.clone();
1185 let ix = self.index;
1186 move |_, window, cx| {
1187 list.update(cx, |this, cx| {
1188 if this.strip_mode != Some(mode) {
1189 this.set_active_breakpoint_property(mode, window, cx);
1190 } else if this.selected_ix == Some(ix) {
1191 this.strip_mode.take();
1192 } else {
1193 cx.propagate();
1194 }
1195 })
1196 .ok();
1197 }
1198 }
1199 fn add_border(
1200 &self,
1201 kind: ActiveBreakpointStripMode,
1202 available: bool,
1203 window: &Window,
1204 cx: &App,
1205 ) -> impl Fn(Div) -> Div {
1206 move |this: Div| {
1207 // Avoid layout shifts in case there's no colored border
1208 let this = this.border_2().rounded_sm();
1209 if self.is_selected && self.strip_mode == Some(kind) {
1210 let theme = cx.theme().colors();
1211 if self.focus_handle.is_focused(window) {
1212 this.border_color(theme.border_selected)
1213 } else {
1214 this.border_color(theme.border_disabled)
1215 }
1216 } else if !available {
1217 this.border_color(cx.theme().colors().border_disabled)
1218 } else {
1219 this
1220 }
1221 }
1222 }
1223}
1224impl RenderOnce for BreakpointOptionsStrip {
1225 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1226 let id = self.breakpoint.id();
1227 let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1228 let supports_condition = self
1229 .props
1230 .contains(SupportedBreakpointProperties::CONDITION);
1231 let supports_hit_condition = self
1232 .props
1233 .contains(SupportedBreakpointProperties::HIT_CONDITION);
1234 let has_logs = self.breakpoint.has_log();
1235 let has_condition = self.breakpoint.has_condition();
1236 let has_hit_condition = self.breakpoint.has_hit_condition();
1237 let style_for_toggle = |mode, is_enabled| {
1238 if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1239 ui::ButtonStyle::Filled
1240 } else {
1241 ui::ButtonStyle::Subtle
1242 }
1243 };
1244 let color_for_toggle = |is_enabled| {
1245 if is_enabled {
1246 ui::Color::Default
1247 } else {
1248 ui::Color::Muted
1249 }
1250 };
1251
1252 h_flex()
1253 .gap_1()
1254 .child(
1255 div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
1256 .child(
1257 IconButton::new(
1258 SharedString::from(format!("{id}-log-toggle")),
1259 IconName::ScrollText,
1260 )
1261 .icon_size(IconSize::XSmall)
1262 .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1263 .icon_color(color_for_toggle(has_logs))
1264 .disabled(!supports_logs)
1265 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1266 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
1267 )
1268 .when(!has_logs && !self.is_selected, |this| this.invisible()),
1269 )
1270 .child(
1271 div().map(self.add_border(
1272 ActiveBreakpointStripMode::Condition,
1273 supports_condition,
1274 window, cx
1275 ))
1276 .child(
1277 IconButton::new(
1278 SharedString::from(format!("{id}-condition-toggle")),
1279 IconName::SplitAlt,
1280 )
1281 .icon_size(IconSize::XSmall)
1282 .style(style_for_toggle(
1283 ActiveBreakpointStripMode::Condition,
1284 has_condition
1285 ))
1286 .icon_color(color_for_toggle(has_condition))
1287 .disabled(!supports_condition)
1288 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1289 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1290 .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
1291 )
1292 .when(!has_condition && !self.is_selected, |this| this.invisible()),
1293 )
1294 .child(
1295 div().map(self.add_border(
1296 ActiveBreakpointStripMode::HitCondition,
1297 supports_hit_condition,window, cx
1298 ))
1299 .child(
1300 IconButton::new(
1301 SharedString::from(format!("{id}-hit-condition-toggle")),
1302 IconName::ArrowDown10,
1303 )
1304 .icon_size(IconSize::XSmall)
1305 .style(style_for_toggle(
1306 ActiveBreakpointStripMode::HitCondition,
1307 has_hit_condition,
1308 ))
1309 .icon_color(color_for_toggle(has_hit_condition))
1310 .disabled(!supports_hit_condition)
1311 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1312 .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
1313 )
1314 .when(!has_hit_condition && !self.is_selected, |this| {
1315 this.invisible()
1316 }),
1317 )
1318 }
1319}