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