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