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