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