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