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