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