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