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_1()
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.clone().map(|dir| {
888 Label::new(dir)
889 .color(Color::Muted)
890 .size(LabelSize::Small)
891 .line_height_style(ui::LineHeightStyle::UiLabel)
892 })),
893 )
894 .child(BreakpointOptionsStrip {
895 props,
896 breakpoint: BreakpointEntry {
897 kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
898 weak: weak,
899 },
900 is_selected,
901 focus_handle,
902 strip_mode,
903 index: ix,
904 }),
905 )
906 .toggle_state(is_selected)
907 }
908}
909#[derive(Clone, Debug)]
910struct ExceptionBreakpoint {
911 id: String,
912 data: ExceptionBreakpointsFilter,
913 is_enabled: bool,
914}
915
916impl ExceptionBreakpoint {
917 fn render(
918 &mut self,
919 props: SupportedBreakpointProperties,
920 strip_mode: Option<ActiveBreakpointStripMode>,
921 ix: usize,
922 is_selected: bool,
923 focus_handle: FocusHandle,
924 list: WeakEntity<BreakpointList>,
925 ) -> ListItem {
926 let color = if self.is_enabled {
927 Color::Debugger
928 } else {
929 Color::Muted
930 };
931 let id = SharedString::from(&self.id);
932 let is_enabled = self.is_enabled;
933 let weak = list.clone();
934 ListItem::new(SharedString::from(format!(
935 "exception-breakpoint-ui-item-{}",
936 self.id
937 )))
938 .on_click({
939 let list = list.clone();
940 move |_, window, cx| {
941 list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
942 .ok();
943 }
944 })
945 .rounded()
946 .on_secondary_mouse_down(|_, _, cx| {
947 cx.stop_propagation();
948 })
949 .start_slot(
950 div()
951 .id(SharedString::from(format!(
952 "exception-breakpoint-ui-item-{}-click-handler",
953 self.id
954 )))
955 .tooltip({
956 let focus_handle = focus_handle.clone();
957 move |window, cx| {
958 Tooltip::for_action_in(
959 if is_enabled {
960 "Disable Exception Breakpoint"
961 } else {
962 "Enable Exception Breakpoint"
963 },
964 &ToggleEnableBreakpoint,
965 &focus_handle,
966 window,
967 cx,
968 )
969 }
970 })
971 .on_click({
972 let list = list.clone();
973 move |_, _, cx| {
974 list.update(cx, |this, cx| {
975 if let Some(session) = &this.session {
976 session.update(cx, |this, cx| {
977 this.toggle_exception_breakpoint(&id, cx);
978 });
979 cx.notify();
980 }
981 })
982 .ok();
983 }
984 })
985 .cursor_pointer()
986 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
987 )
988 .child(
989 h_flex()
990 .w_full()
991 .mr_4()
992 .py_0p5()
993 .justify_between()
994 .child(
995 v_flex()
996 .py_1()
997 .gap_1()
998 .min_h(px(26.))
999 .justify_center()
1000 .id(("exception-breakpoint-label", ix))
1001 .child(
1002 Label::new(self.data.label.clone())
1003 .size(LabelSize::Small)
1004 .line_height_style(ui::LineHeightStyle::UiLabel),
1005 )
1006 .when_some(self.data.description.clone(), |el, description| {
1007 el.tooltip(Tooltip::text(description))
1008 }),
1009 )
1010 .child(BreakpointOptionsStrip {
1011 props,
1012 breakpoint: BreakpointEntry {
1013 kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1014 weak: weak,
1015 },
1016 is_selected,
1017 focus_handle,
1018 strip_mode,
1019 index: ix,
1020 }),
1021 )
1022 .toggle_state(is_selected)
1023 }
1024}
1025#[derive(Clone, Debug)]
1026enum BreakpointEntryKind {
1027 LineBreakpoint(LineBreakpoint),
1028 ExceptionBreakpoint(ExceptionBreakpoint),
1029}
1030
1031#[derive(Clone, Debug)]
1032struct BreakpointEntry {
1033 kind: BreakpointEntryKind,
1034 weak: WeakEntity<BreakpointList>,
1035}
1036
1037impl BreakpointEntry {
1038 fn render(
1039 &mut self,
1040 strip_mode: Option<ActiveBreakpointStripMode>,
1041 props: SupportedBreakpointProperties,
1042 ix: usize,
1043 is_selected: bool,
1044 focus_handle: FocusHandle,
1045 ) -> ListItem {
1046 match &mut self.kind {
1047 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1048 props,
1049 strip_mode,
1050 ix,
1051 is_selected,
1052 focus_handle,
1053 self.weak.clone(),
1054 ),
1055 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1056 .render(
1057 props.for_exception_breakpoints(),
1058 strip_mode,
1059 ix,
1060 is_selected,
1061 focus_handle,
1062 self.weak.clone(),
1063 ),
1064 }
1065 }
1066
1067 fn id(&self) -> SharedString {
1068 match &self.kind {
1069 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1070 "source-breakpoint-control-strip-{:?}:{}",
1071 line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1072 )
1073 .into(),
1074 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1075 "exception-breakpoint-control-strip--{}",
1076 exception_breakpoint.id
1077 )
1078 .into(),
1079 }
1080 }
1081
1082 fn has_log(&self) -> bool {
1083 match &self.kind {
1084 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1085 line_breakpoint.breakpoint.message.is_some()
1086 }
1087 _ => false,
1088 }
1089 }
1090
1091 fn has_condition(&self) -> bool {
1092 match &self.kind {
1093 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1094 line_breakpoint.breakpoint.condition.is_some()
1095 }
1096 // We don't support conditions on exception breakpoints
1097 BreakpointEntryKind::ExceptionBreakpoint(_) => false,
1098 }
1099 }
1100
1101 fn has_hit_condition(&self) -> bool {
1102 match &self.kind {
1103 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1104 line_breakpoint.breakpoint.hit_condition.is_some()
1105 }
1106 _ => false,
1107 }
1108 }
1109}
1110bitflags::bitflags! {
1111 #[derive(Clone, Copy)]
1112 pub struct SupportedBreakpointProperties: u32 {
1113 const LOG = 1 << 0;
1114 const CONDITION = 1 << 1;
1115 const HIT_CONDITION = 1 << 2;
1116 // Conditions for exceptions can be set only when exception filters are supported.
1117 const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1118 }
1119}
1120
1121impl From<&Capabilities> for SupportedBreakpointProperties {
1122 fn from(caps: &Capabilities) -> Self {
1123 let mut this = Self::empty();
1124 for (prop, offset) in [
1125 (caps.supports_log_points, Self::LOG),
1126 (caps.supports_conditional_breakpoints, Self::CONDITION),
1127 (
1128 caps.supports_hit_conditional_breakpoints,
1129 Self::HIT_CONDITION,
1130 ),
1131 (
1132 caps.supports_exception_options,
1133 Self::EXCEPTION_FILTER_OPTIONS,
1134 ),
1135 ] {
1136 if prop.unwrap_or_default() {
1137 this.insert(offset);
1138 }
1139 }
1140 this
1141 }
1142}
1143
1144impl SupportedBreakpointProperties {
1145 fn for_exception_breakpoints(self) -> Self {
1146 // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1147 Self::empty()
1148 }
1149}
1150#[derive(IntoElement)]
1151struct BreakpointOptionsStrip {
1152 props: SupportedBreakpointProperties,
1153 breakpoint: BreakpointEntry,
1154 is_selected: bool,
1155 focus_handle: FocusHandle,
1156 strip_mode: Option<ActiveBreakpointStripMode>,
1157 index: usize,
1158}
1159
1160impl BreakpointOptionsStrip {
1161 fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1162 self.is_selected && self.strip_mode == Some(expected_mode)
1163 }
1164 fn on_click_callback(
1165 &self,
1166 mode: ActiveBreakpointStripMode,
1167 ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1168 let list = self.breakpoint.weak.clone();
1169 let ix = self.index;
1170 move |_, window, cx| {
1171 list.update(cx, |this, cx| {
1172 if this.strip_mode != Some(mode) {
1173 this.set_active_breakpoint_property(mode, window, cx);
1174 } else if this.selected_ix == Some(ix) {
1175 this.strip_mode.take();
1176 } else {
1177 cx.propagate();
1178 }
1179 })
1180 .ok();
1181 }
1182 }
1183 fn add_border(
1184 &self,
1185 kind: ActiveBreakpointStripMode,
1186 available: bool,
1187 window: &Window,
1188 cx: &App,
1189 ) -> impl Fn(Div) -> Div {
1190 move |this: Div| {
1191 // Avoid layout shifts in case there's no colored border
1192 let this = this.border_2().rounded_sm();
1193 if self.is_selected && self.strip_mode == Some(kind) {
1194 let theme = cx.theme().colors();
1195 if self.focus_handle.is_focused(window) {
1196 this.border_color(theme.border_selected)
1197 } else {
1198 this.border_color(theme.border_disabled)
1199 }
1200 } else if !available {
1201 this.border_color(cx.theme().colors().border_disabled)
1202 } else {
1203 this
1204 }
1205 }
1206 }
1207}
1208impl RenderOnce for BreakpointOptionsStrip {
1209 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1210 let id = self.breakpoint.id();
1211 let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1212 let supports_condition = self
1213 .props
1214 .contains(SupportedBreakpointProperties::CONDITION);
1215 let supports_hit_condition = self
1216 .props
1217 .contains(SupportedBreakpointProperties::HIT_CONDITION);
1218 let has_logs = self.breakpoint.has_log();
1219 let has_condition = self.breakpoint.has_condition();
1220 let has_hit_condition = self.breakpoint.has_hit_condition();
1221 let style_for_toggle = |mode, is_enabled| {
1222 if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1223 ui::ButtonStyle::Filled
1224 } else {
1225 ui::ButtonStyle::Subtle
1226 }
1227 };
1228 let color_for_toggle = |is_enabled| {
1229 if is_enabled {
1230 ui::Color::Default
1231 } else {
1232 ui::Color::Muted
1233 }
1234 };
1235
1236 h_flex()
1237 .gap_2()
1238 .child(
1239 div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
1240 .child(
1241 IconButton::new(
1242 SharedString::from(format!("{id}-log-toggle")),
1243 IconName::ScrollText,
1244 )
1245 .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1246 .icon_color(color_for_toggle(has_logs))
1247 .disabled(!supports_logs)
1248 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1249 .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))
1250 )
1251 .when(!has_logs && !self.is_selected, |this| this.invisible()),
1252 )
1253 .child(
1254 div().map(self.add_border(
1255 ActiveBreakpointStripMode::Condition,
1256 supports_condition,
1257 window, cx
1258 ))
1259 .child(
1260 IconButton::new(
1261 SharedString::from(format!("{id}-condition-toggle")),
1262 IconName::SplitAlt,
1263 )
1264 .style(style_for_toggle(
1265 ActiveBreakpointStripMode::Condition,
1266 has_condition
1267 ))
1268 .icon_color(color_for_toggle(has_condition))
1269 .disabled(!supports_condition)
1270 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1271 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1272 .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))
1273 )
1274 .when(!has_condition && !self.is_selected, |this| this.invisible()),
1275 )
1276 .child(
1277 div() .map(self.add_border(
1278 ActiveBreakpointStripMode::HitCondition,
1279 supports_hit_condition,window, cx
1280 ))
1281 .child(
1282 IconButton::new(
1283 SharedString::from(format!("{id}-hit-condition-toggle")),
1284 IconName::ArrowDown10,
1285 )
1286 .style(style_for_toggle(
1287 ActiveBreakpointStripMode::HitCondition,
1288 has_hit_condition,
1289 ))
1290 .icon_color(color_for_toggle(has_hit_condition))
1291 .disabled(!supports_hit_condition)
1292 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1293 .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))
1294 )
1295 .when(!has_hit_condition && !self.is_selected, |this| {
1296 this.invisible()
1297 }),
1298 )
1299 }
1300}