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 window,
617 cx,
618 )
619 }
620 })
621 })
622 .disabled(selection_kind.is_none())
623 .on_click({
624 let focus_handle = focus_handle.clone();
625 move |_, window, cx| {
626 focus_handle.focus(window);
627 window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
628 }
629 }),
630 )
631 .child(
632 IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
633 .icon_size(IconSize::Small)
634 .when_some(remove_breakpoint_tooltip, |this, tooltip| {
635 this.tooltip({
636 let focus_handle = focus_handle.clone();
637 move |window, cx| {
638 Tooltip::with_meta_in(
639 "Remove Breakpoint",
640 Some(&UnsetBreakpoint),
641 tooltip,
642 &focus_handle,
643 window,
644 cx,
645 )
646 }
647 })
648 })
649 .disabled(
650 selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
651 )
652 .on_click({
653 move |_, window, cx| {
654 focus_handle.focus(window);
655 window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
656 }
657 }),
658 )
659 .into_any_element()
660 }
661}
662
663impl Render for BreakpointList {
664 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
665 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
666 self.breakpoints.clear();
667 let path_style = self.worktree_store.read(cx).path_style();
668 let weak = cx.weak_entity();
669 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
670 let relative_worktree_path = self
671 .worktree_store
672 .read(cx)
673 .find_worktree(&path, cx)
674 .and_then(|(worktree, relative_path)| {
675 worktree
676 .read(cx)
677 .is_visible()
678 .then(|| worktree.read(cx).root_name().join(&relative_path))
679 });
680 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
681 let weak = weak.clone();
682 breakpoints.into_iter().filter_map(move |breakpoint| {
683 debug_assert_eq!(&path, &breakpoint.path);
684 let file_name = breakpoint.path.file_name()?;
685 let breakpoint_path = RelPath::new(&breakpoint.path, path_style).ok();
686
687 let dir = relative_worktree_path
688 .as_deref()
689 .or(breakpoint_path.as_deref())?
690 .parent()
691 .map(|parent| SharedString::from(parent.display(path_style).to_string()));
692 let name = file_name
693 .to_str()
694 .map(ToOwned::to_owned)
695 .map(SharedString::from)?;
696 let weak = weak.clone();
697 let line = breakpoint.row + 1;
698 Some(BreakpointEntry {
699 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
700 name,
701 dir,
702 line,
703 breakpoint,
704 }),
705 weak,
706 })
707 })
708 });
709 let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
710 session
711 .read(cx)
712 .exception_breakpoints()
713 .map(|(data, is_enabled)| BreakpointEntry {
714 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
715 id: data.filter.clone(),
716 data: data.clone(),
717 is_enabled: *is_enabled,
718 }),
719 weak: weak.clone(),
720 })
721 });
722 let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
723 session
724 .read(cx)
725 .data_breakpoints()
726 .map(|state| BreakpointEntry {
727 kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
728 weak: weak.clone(),
729 })
730 });
731 self.breakpoints.extend(
732 breakpoints
733 .chain(data_breakpoints)
734 .chain(exception_breakpoints),
735 );
736
737 v_flex()
738 .id("breakpoint-list")
739 .key_context("BreakpointList")
740 .track_focus(&self.focus_handle)
741 .on_action(cx.listener(Self::select_next))
742 .on_action(cx.listener(Self::select_previous))
743 .on_action(cx.listener(Self::select_first))
744 .on_action(cx.listener(Self::select_last))
745 .on_action(cx.listener(Self::dismiss))
746 .on_action(cx.listener(Self::confirm))
747 .on_action(cx.listener(Self::toggle_enable_breakpoint))
748 .on_action(cx.listener(Self::unset_breakpoint))
749 .on_action(cx.listener(Self::next_breakpoint_property))
750 .on_action(cx.listener(Self::previous_breakpoint_property))
751 .size_full()
752 .pt_1()
753 .child(self.render_list(cx))
754 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
755 .when_some(self.strip_mode, |this, _| {
756 this.child(Divider::horizontal().color(DividerColor::Border))
757 .child(
758 h_flex()
759 .p_1()
760 .rounded_sm()
761 .bg(cx.theme().colors().editor_background)
762 .border_1()
763 .when(
764 self.input.focus_handle(cx).contains_focused(window, cx),
765 |this| {
766 let colors = cx.theme().colors();
767
768 let border_color = if self.input.read(cx).read_only(cx) {
769 colors.border_disabled
770 } else {
771 colors.border_transparent
772 };
773
774 this.border_color(border_color)
775 },
776 )
777 .child(self.input.clone()),
778 )
779 })
780 }
781}
782
783#[derive(Clone, Debug)]
784struct LineBreakpoint {
785 name: SharedString,
786 dir: Option<SharedString>,
787 line: u32,
788 breakpoint: SourceBreakpoint,
789}
790
791impl LineBreakpoint {
792 fn render(
793 &mut self,
794 props: SupportedBreakpointProperties,
795 strip_mode: Option<ActiveBreakpointStripMode>,
796 ix: usize,
797 is_selected: bool,
798 focus_handle: FocusHandle,
799 weak: WeakEntity<BreakpointList>,
800 ) -> ListItem {
801 let icon_name = if self.breakpoint.state.is_enabled() {
802 IconName::DebugBreakpoint
803 } else {
804 IconName::DebugDisabledBreakpoint
805 };
806 let path = self.breakpoint.path.clone();
807 let row = self.breakpoint.row;
808 let is_enabled = self.breakpoint.state.is_enabled();
809
810 let indicator = div()
811 .id(SharedString::from(format!(
812 "breakpoint-ui-toggle-{:?}/{}:{}",
813 self.dir, self.name, self.line
814 )))
815 .child(
816 Icon::new(icon_name)
817 .color(Color::Debugger)
818 .size(IconSize::XSmall),
819 )
820 .tooltip({
821 let focus_handle = focus_handle.clone();
822 move |window, cx| {
823 Tooltip::for_action_in(
824 if is_enabled {
825 "Disable Breakpoint"
826 } else {
827 "Enable Breakpoint"
828 },
829 &ToggleEnableBreakpoint,
830 &focus_handle,
831 window,
832 cx,
833 )
834 }
835 })
836 .on_click({
837 let weak = weak.clone();
838 let path = path.clone();
839 move |_, _, cx| {
840 weak.update(cx, |breakpoint_list, cx| {
841 breakpoint_list.edit_line_breakpoint(
842 path.clone(),
843 row,
844 BreakpointEditAction::InvertState,
845 cx,
846 );
847 })
848 .ok();
849 }
850 })
851 .on_mouse_down(MouseButton::Left, move |_, _, _| {});
852
853 ListItem::new(SharedString::from(format!(
854 "breakpoint-ui-item-{:?}/{}:{}",
855 self.dir, self.name, self.line
856 )))
857 .toggle_state(is_selected)
858 .inset(true)
859 .on_click({
860 let weak = weak.clone();
861 move |_, window, cx| {
862 weak.update(cx, |breakpoint_list, cx| {
863 breakpoint_list.select_ix(Some(ix), window, cx);
864 })
865 .ok();
866 }
867 })
868 .on_secondary_mouse_down(|_, _, cx| {
869 cx.stop_propagation();
870 })
871 .start_slot(indicator)
872 .child(
873 h_flex()
874 .id(SharedString::from(format!(
875 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
876 self.dir, self.name, self.line
877 )))
878 .w_full()
879 .gap_1()
880 .min_h(rems_from_px(26.))
881 .justify_between()
882 .on_click({
883 let weak = weak.clone();
884 move |_, window, cx| {
885 weak.update(cx, |breakpoint_list, cx| {
886 breakpoint_list.select_ix(Some(ix), window, cx);
887 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
888 })
889 .ok();
890 }
891 })
892 .child(
893 h_flex()
894 .id("label-container")
895 .gap_0p5()
896 .child(
897 Label::new(format!("{}:{}", self.name, self.line))
898 .size(LabelSize::Small)
899 .line_height_style(ui::LineHeightStyle::UiLabel),
900 )
901 .children(self.dir.as_ref().and_then(|dir| {
902 let path_without_root = Path::new(dir.as_ref())
903 .components()
904 .skip(1)
905 .collect::<PathBuf>();
906 path_without_root.components().next()?;
907 Some(
908 Label::new(path_without_root.to_string_lossy().into_owned())
909 .color(Color::Muted)
910 .size(LabelSize::Small)
911 .line_height_style(ui::LineHeightStyle::UiLabel)
912 .truncate(),
913 )
914 }))
915 .when_some(self.dir.as_ref(), |this, parent_dir| {
916 this.tooltip(Tooltip::text(format!(
917 "Worktree parent path: {parent_dir}"
918 )))
919 }),
920 )
921 .child(BreakpointOptionsStrip {
922 props,
923 breakpoint: BreakpointEntry {
924 kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
925 weak,
926 },
927 is_selected,
928 focus_handle,
929 strip_mode,
930 index: ix,
931 }),
932 )
933 }
934}
935
936#[derive(Clone, Debug)]
937struct ExceptionBreakpoint {
938 id: String,
939 data: ExceptionBreakpointsFilter,
940 is_enabled: bool,
941}
942
943#[derive(Clone, Debug)]
944struct DataBreakpoint(project::debugger::session::DataBreakpointState);
945
946impl DataBreakpoint {
947 fn render(
948 &self,
949 props: SupportedBreakpointProperties,
950 strip_mode: Option<ActiveBreakpointStripMode>,
951 ix: usize,
952 is_selected: bool,
953 focus_handle: FocusHandle,
954 list: WeakEntity<BreakpointList>,
955 ) -> ListItem {
956 let color = if self.0.is_enabled {
957 Color::Debugger
958 } else {
959 Color::Muted
960 };
961 let is_enabled = self.0.is_enabled;
962 let id = self.0.dap.data_id.clone();
963
964 ListItem::new(SharedString::from(format!(
965 "data-breakpoint-ui-item-{}",
966 self.0.dap.data_id
967 )))
968 .toggle_state(is_selected)
969 .inset(true)
970 .start_slot(
971 div()
972 .id(SharedString::from(format!(
973 "data-breakpoint-ui-item-{}-click-handler",
974 self.0.dap.data_id
975 )))
976 .child(
977 Icon::new(IconName::Binary)
978 .color(color)
979 .size(IconSize::Small),
980 )
981 .tooltip({
982 let focus_handle = focus_handle.clone();
983 move |window, cx| {
984 Tooltip::for_action_in(
985 if is_enabled {
986 "Disable Data Breakpoint"
987 } else {
988 "Enable Data Breakpoint"
989 },
990 &ToggleEnableBreakpoint,
991 &focus_handle,
992 window,
993 cx,
994 )
995 }
996 })
997 .on_click({
998 let list = list.clone();
999 move |_, _, cx| {
1000 list.update(cx, |this, cx| {
1001 this.toggle_data_breakpoint(&id, cx);
1002 })
1003 .ok();
1004 }
1005 }),
1006 )
1007 .child(
1008 h_flex()
1009 .w_full()
1010 .gap_1()
1011 .min_h(rems_from_px(26.))
1012 .justify_between()
1013 .child(
1014 v_flex()
1015 .py_1()
1016 .gap_1()
1017 .justify_center()
1018 .id(("data-breakpoint-label", ix))
1019 .child(
1020 Label::new(self.0.context.human_readable_label())
1021 .size(LabelSize::Small)
1022 .line_height_style(ui::LineHeightStyle::UiLabel),
1023 ),
1024 )
1025 .child(BreakpointOptionsStrip {
1026 props,
1027 breakpoint: BreakpointEntry {
1028 kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1029 weak: list,
1030 },
1031 is_selected,
1032 focus_handle,
1033 strip_mode,
1034 index: ix,
1035 }),
1036 )
1037 }
1038}
1039
1040impl ExceptionBreakpoint {
1041 fn render(
1042 &mut self,
1043 props: SupportedBreakpointProperties,
1044 strip_mode: Option<ActiveBreakpointStripMode>,
1045 ix: usize,
1046 is_selected: bool,
1047 focus_handle: FocusHandle,
1048 list: WeakEntity<BreakpointList>,
1049 ) -> ListItem {
1050 let color = if self.is_enabled {
1051 Color::Debugger
1052 } else {
1053 Color::Muted
1054 };
1055 let id = SharedString::from(&self.id);
1056 let is_enabled = self.is_enabled;
1057 let weak = list.clone();
1058
1059 ListItem::new(SharedString::from(format!(
1060 "exception-breakpoint-ui-item-{}",
1061 self.id
1062 )))
1063 .toggle_state(is_selected)
1064 .inset(true)
1065 .on_click({
1066 let list = list.clone();
1067 move |_, window, cx| {
1068 list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1069 .ok();
1070 }
1071 })
1072 .on_secondary_mouse_down(|_, _, cx| {
1073 cx.stop_propagation();
1074 })
1075 .start_slot(
1076 div()
1077 .id(SharedString::from(format!(
1078 "exception-breakpoint-ui-item-{}-click-handler",
1079 self.id
1080 )))
1081 .child(
1082 Icon::new(IconName::Flame)
1083 .color(color)
1084 .size(IconSize::Small),
1085 )
1086 .tooltip({
1087 let focus_handle = focus_handle.clone();
1088 move |window, cx| {
1089 Tooltip::for_action_in(
1090 if is_enabled {
1091 "Disable Exception Breakpoint"
1092 } else {
1093 "Enable Exception Breakpoint"
1094 },
1095 &ToggleEnableBreakpoint,
1096 &focus_handle,
1097 window,
1098 cx,
1099 )
1100 }
1101 })
1102 .on_click({
1103 move |_, _, cx| {
1104 list.update(cx, |this, cx| {
1105 this.toggle_exception_breakpoint(&id, cx);
1106 })
1107 .ok();
1108 }
1109 }),
1110 )
1111 .child(
1112 h_flex()
1113 .w_full()
1114 .gap_1()
1115 .min_h(rems_from_px(26.))
1116 .justify_between()
1117 .child(
1118 v_flex()
1119 .py_1()
1120 .gap_1()
1121 .justify_center()
1122 .id(("exception-breakpoint-label", ix))
1123 .child(
1124 Label::new(self.data.label.clone())
1125 .size(LabelSize::Small)
1126 .line_height_style(ui::LineHeightStyle::UiLabel),
1127 )
1128 .when_some(self.data.description.clone(), |el, description| {
1129 el.tooltip(Tooltip::text(description))
1130 }),
1131 )
1132 .child(BreakpointOptionsStrip {
1133 props,
1134 breakpoint: BreakpointEntry {
1135 kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1136 weak,
1137 },
1138 is_selected,
1139 focus_handle,
1140 strip_mode,
1141 index: ix,
1142 }),
1143 )
1144 }
1145}
1146#[derive(Clone, Debug)]
1147enum BreakpointEntryKind {
1148 LineBreakpoint(LineBreakpoint),
1149 ExceptionBreakpoint(ExceptionBreakpoint),
1150 DataBreakpoint(DataBreakpoint),
1151}
1152
1153#[derive(Clone, Debug)]
1154struct BreakpointEntry {
1155 kind: BreakpointEntryKind,
1156 weak: WeakEntity<BreakpointList>,
1157}
1158
1159impl BreakpointEntry {
1160 fn render(
1161 &mut self,
1162 strip_mode: Option<ActiveBreakpointStripMode>,
1163 props: SupportedBreakpointProperties,
1164 ix: usize,
1165 is_selected: bool,
1166 focus_handle: FocusHandle,
1167 ) -> ListItem {
1168 match &mut self.kind {
1169 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1170 props,
1171 strip_mode,
1172 ix,
1173 is_selected,
1174 focus_handle,
1175 self.weak.clone(),
1176 ),
1177 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1178 .render(
1179 props.for_exception_breakpoints(),
1180 strip_mode,
1181 ix,
1182 is_selected,
1183 focus_handle,
1184 self.weak.clone(),
1185 ),
1186 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1187 props.for_data_breakpoints(),
1188 strip_mode,
1189 ix,
1190 is_selected,
1191 focus_handle,
1192 self.weak.clone(),
1193 ),
1194 }
1195 }
1196
1197 fn id(&self) -> SharedString {
1198 match &self.kind {
1199 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1200 "source-breakpoint-control-strip-{:?}:{}",
1201 line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1202 )
1203 .into(),
1204 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1205 "exception-breakpoint-control-strip--{}",
1206 exception_breakpoint.id
1207 )
1208 .into(),
1209 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1210 "data-breakpoint-control-strip--{}",
1211 data_breakpoint.0.dap.data_id
1212 )
1213 .into(),
1214 }
1215 }
1216
1217 fn has_log(&self) -> bool {
1218 match &self.kind {
1219 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1220 line_breakpoint.breakpoint.message.is_some()
1221 }
1222 _ => false,
1223 }
1224 }
1225
1226 fn has_condition(&self) -> bool {
1227 match &self.kind {
1228 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1229 line_breakpoint.breakpoint.condition.is_some()
1230 }
1231 // We don't support conditions on exception/data breakpoints
1232 _ => false,
1233 }
1234 }
1235
1236 fn has_hit_condition(&self) -> bool {
1237 match &self.kind {
1238 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1239 line_breakpoint.breakpoint.hit_condition.is_some()
1240 }
1241 _ => false,
1242 }
1243 }
1244}
1245
1246bitflags::bitflags! {
1247 #[derive(Clone, Copy)]
1248 pub struct SupportedBreakpointProperties: u32 {
1249 const LOG = 1 << 0;
1250 const CONDITION = 1 << 1;
1251 const HIT_CONDITION = 1 << 2;
1252 // Conditions for exceptions can be set only when exception filters are supported.
1253 const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1254 }
1255}
1256
1257impl From<&Capabilities> for SupportedBreakpointProperties {
1258 fn from(caps: &Capabilities) -> Self {
1259 let mut this = Self::empty();
1260 for (prop, offset) in [
1261 (caps.supports_log_points, Self::LOG),
1262 (caps.supports_conditional_breakpoints, Self::CONDITION),
1263 (
1264 caps.supports_hit_conditional_breakpoints,
1265 Self::HIT_CONDITION,
1266 ),
1267 (
1268 caps.supports_exception_options,
1269 Self::EXCEPTION_FILTER_OPTIONS,
1270 ),
1271 ] {
1272 if prop.unwrap_or_default() {
1273 this.insert(offset);
1274 }
1275 }
1276 this
1277 }
1278}
1279
1280impl SupportedBreakpointProperties {
1281 fn for_exception_breakpoints(self) -> Self {
1282 // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1283 Self::empty()
1284 }
1285 fn for_data_breakpoints(self) -> Self {
1286 // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1287 Self::empty()
1288 }
1289}
1290#[derive(IntoElement)]
1291struct BreakpointOptionsStrip {
1292 props: SupportedBreakpointProperties,
1293 breakpoint: BreakpointEntry,
1294 is_selected: bool,
1295 focus_handle: FocusHandle,
1296 strip_mode: Option<ActiveBreakpointStripMode>,
1297 index: usize,
1298}
1299
1300impl BreakpointOptionsStrip {
1301 fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1302 self.is_selected && self.strip_mode == Some(expected_mode)
1303 }
1304
1305 fn on_click_callback(
1306 &self,
1307 mode: ActiveBreakpointStripMode,
1308 ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1309 let list = self.breakpoint.weak.clone();
1310 let ix = self.index;
1311 move |_, window, cx| {
1312 list.update(cx, |this, cx| {
1313 if this.strip_mode != Some(mode) {
1314 this.set_active_breakpoint_property(mode, window, cx);
1315 } else if this.selected_ix == Some(ix) {
1316 this.strip_mode.take();
1317 } else {
1318 cx.propagate();
1319 }
1320 })
1321 .ok();
1322 }
1323 }
1324
1325 fn add_focus_styles(
1326 &self,
1327 kind: ActiveBreakpointStripMode,
1328 available: bool,
1329 window: &Window,
1330 cx: &App,
1331 ) -> impl Fn(Div) -> Div {
1332 move |this: Div| {
1333 // Avoid layout shifts in case there's no colored border
1334 let this = this.border_1().rounded_sm();
1335 let color = cx.theme().colors();
1336
1337 if self.is_selected && self.strip_mode == Some(kind) {
1338 if self.focus_handle.is_focused(window) {
1339 this.bg(color.editor_background)
1340 .border_color(color.border_focused)
1341 } else {
1342 this.border_color(color.border)
1343 }
1344 } else if !available {
1345 this.border_color(color.border_transparent)
1346 } else {
1347 this
1348 }
1349 }
1350 }
1351}
1352
1353impl RenderOnce for BreakpointOptionsStrip {
1354 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1355 let id = self.breakpoint.id();
1356 let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1357 let supports_condition = self
1358 .props
1359 .contains(SupportedBreakpointProperties::CONDITION);
1360 let supports_hit_condition = self
1361 .props
1362 .contains(SupportedBreakpointProperties::HIT_CONDITION);
1363 let has_logs = self.breakpoint.has_log();
1364 let has_condition = self.breakpoint.has_condition();
1365 let has_hit_condition = self.breakpoint.has_hit_condition();
1366 let style_for_toggle = |mode, is_enabled| {
1367 if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1368 ui::ButtonStyle::Filled
1369 } else {
1370 ui::ButtonStyle::Subtle
1371 }
1372 };
1373 let color_for_toggle = |is_enabled| {
1374 if is_enabled {
1375 Color::Default
1376 } else {
1377 Color::Muted
1378 }
1379 };
1380
1381 h_flex()
1382 .gap_px()
1383 .mr_3() // Space to avoid overlapping with the scrollbar
1384 .child(
1385 div()
1386 .map(self.add_focus_styles(
1387 ActiveBreakpointStripMode::Log,
1388 supports_logs,
1389 window,
1390 cx,
1391 ))
1392 .child(
1393 IconButton::new(
1394 SharedString::from(format!("{id}-log-toggle")),
1395 IconName::Notepad,
1396 )
1397 .shape(ui::IconButtonShape::Square)
1398 .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1399 .icon_size(IconSize::Small)
1400 .icon_color(color_for_toggle(has_logs))
1401 .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
1402 .disabled(!supports_logs)
1403 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1404 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
1405 .tooltip(|window, cx| {
1406 Tooltip::with_meta(
1407 "Set Log Message",
1408 None,
1409 "Set log message to display (instead of stopping) when a breakpoint is hit.",
1410 window,
1411 cx,
1412 )
1413 }),
1414 )
1415 .when(!has_logs && !self.is_selected, |this| this.invisible()),
1416 )
1417 .child(
1418 div()
1419 .map(self.add_focus_styles(
1420 ActiveBreakpointStripMode::Condition,
1421 supports_condition,
1422 window,
1423 cx,
1424 ))
1425 .child(
1426 IconButton::new(
1427 SharedString::from(format!("{id}-condition-toggle")),
1428 IconName::SplitAlt,
1429 )
1430 .shape(ui::IconButtonShape::Square)
1431 .style(style_for_toggle(
1432 ActiveBreakpointStripMode::Condition,
1433 has_condition,
1434 ))
1435 .icon_size(IconSize::Small)
1436 .icon_color(color_for_toggle(has_condition))
1437 .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1438 .disabled(!supports_condition)
1439 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1440 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1441 .tooltip(|window, cx| {
1442 Tooltip::with_meta(
1443 "Set Condition",
1444 None,
1445 "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
1446 window,
1447 cx,
1448 )
1449 }),
1450 )
1451 .when(!has_condition && !self.is_selected, |this| this.invisible()),
1452 )
1453 .child(
1454 div()
1455 .map(self.add_focus_styles(
1456 ActiveBreakpointStripMode::HitCondition,
1457 supports_hit_condition,
1458 window,
1459 cx,
1460 ))
1461 .child(
1462 IconButton::new(
1463 SharedString::from(format!("{id}-hit-condition-toggle")),
1464 IconName::ArrowDown10,
1465 )
1466 .style(style_for_toggle(
1467 ActiveBreakpointStripMode::HitCondition,
1468 has_hit_condition,
1469 ))
1470 .shape(ui::IconButtonShape::Square)
1471 .icon_size(IconSize::Small)
1472 .icon_color(color_for_toggle(has_hit_condition))
1473 .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1474 .disabled(!supports_hit_condition)
1475 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1476 .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
1477 .tooltip(|window, cx| {
1478 Tooltip::with_meta(
1479 "Set Hit Condition",
1480 None,
1481 "Set expression that controls how many hits of the breakpoint are ignored.",
1482 window,
1483 cx,
1484 )
1485 }),
1486 )
1487 .when(!has_hit_condition && !self.is_selected, |this| {
1488 this.invisible()
1489 }),
1490 )
1491 }
1492}