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 => match &entry.kind {
333 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
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 },
344 ActiveBreakpointStripMode::Condition => match &entry.kind {
345 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
346 Self::edit_line_breakpoint_inner(
347 &self.breakpoint_store,
348 line_breakpoint.breakpoint.path.clone(),
349 line_breakpoint.breakpoint.row,
350 BreakpointEditAction::EditCondition(Arc::from(text)),
351 cx,
352 );
353 }
354 _ => {}
355 },
356 ActiveBreakpointStripMode::HitCondition => match &entry.kind {
357 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
358 Self::edit_line_breakpoint_inner(
359 &self.breakpoint_store,
360 line_breakpoint.breakpoint.path.clone(),
361 line_breakpoint.breakpoint.row,
362 BreakpointEditAction::EditHitCondition(Arc::from(text)),
363 cx,
364 );
365 }
366 _ => {}
367 },
368 }
369 self.focus_handle.focus(window);
370 } else {
371 handle.focus(window);
372 }
373
374 return;
375 }
376 match &mut entry.kind {
377 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
378 let path = line_breakpoint.breakpoint.path.clone();
379 let row = line_breakpoint.breakpoint.row;
380 self.go_to_line_breakpoint(path, row, window, cx);
381 }
382 BreakpointEntryKind::DataBreakpoint(_)
383 | BreakpointEntryKind::ExceptionBreakpoint(_) => {}
384 }
385 }
386
387 fn toggle_enable_breakpoint(
388 &mut self,
389 _: &ToggleEnableBreakpoint,
390 window: &mut Window,
391 cx: &mut Context<Self>,
392 ) {
393 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
394 return;
395 };
396 if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
397 cx.propagate();
398 return;
399 }
400
401 match &mut entry.kind {
402 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
403 let path = line_breakpoint.breakpoint.path.clone();
404 let row = line_breakpoint.breakpoint.row;
405 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
406 }
407 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
408 let id = exception_breakpoint.id.clone();
409 self.toggle_exception_breakpoint(&id, cx);
410 }
411 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
412 let id = data_breakpoint.0.dap.data_id.clone();
413 self.toggle_data_breakpoint(&id, cx);
414 }
415 }
416 cx.notify();
417 }
418
419 fn unset_breakpoint(
420 &mut self,
421 _: &UnsetBreakpoint,
422 _window: &mut Window,
423 cx: &mut Context<Self>,
424 ) {
425 let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
426 return;
427 };
428
429 match &mut entry.kind {
430 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
431 let path = line_breakpoint.breakpoint.path.clone();
432 let row = line_breakpoint.breakpoint.row;
433 self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
434 }
435 _ => {}
436 }
437 cx.notify();
438 }
439
440 fn previous_breakpoint_property(
441 &mut self,
442 _: &PreviousBreakpointProperty,
443 window: &mut Window,
444 cx: &mut Context<Self>,
445 ) {
446 let next_mode = match self.strip_mode {
447 Some(ActiveBreakpointStripMode::Log) => None,
448 Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
449 Some(ActiveBreakpointStripMode::HitCondition) => {
450 Some(ActiveBreakpointStripMode::Condition)
451 }
452 None => Some(ActiveBreakpointStripMode::HitCondition),
453 };
454 if let Some(mode) = next_mode {
455 self.set_active_breakpoint_property(mode, window, cx);
456 } else {
457 self.strip_mode.take();
458 }
459
460 cx.notify();
461 }
462 fn next_breakpoint_property(
463 &mut self,
464 _: &NextBreakpointProperty,
465 window: &mut Window,
466 cx: &mut Context<Self>,
467 ) {
468 let next_mode = match self.strip_mode {
469 Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
470 Some(ActiveBreakpointStripMode::Condition) => {
471 Some(ActiveBreakpointStripMode::HitCondition)
472 }
473 Some(ActiveBreakpointStripMode::HitCondition) => None,
474 None => Some(ActiveBreakpointStripMode::Log),
475 };
476 if let Some(mode) = next_mode {
477 self.set_active_breakpoint_property(mode, window, cx);
478 } else {
479 self.strip_mode.take();
480 }
481 cx.notify();
482 }
483
484 fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
485 if let Some(session) = &self.session {
486 session.update(cx, |this, cx| {
487 this.toggle_data_breakpoint(id, cx);
488 });
489 }
490 }
491
492 fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
493 if let Some(session) = &self.session {
494 session.update(cx, |this, cx| {
495 this.toggle_exception_breakpoint(id, cx);
496 });
497 cx.notify();
498 const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
499 self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
500 cx.background_executor()
501 .timer(EXCEPTION_SERIALIZATION_INTERVAL)
502 .await;
503 this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
504 .await?;
505 Ok(())
506 }));
507 }
508 }
509
510 fn kvp_key(adapter_name: &str) -> String {
511 format!("debug_adapter_`{adapter_name}`_persistence")
512 }
513 fn serialize_exception_breakpoints(
514 &mut self,
515 cx: &mut Context<Self>,
516 ) -> Task<anyhow::Result<()>> {
517 if let Some(session) = self.session.as_ref() {
518 let key = {
519 let session = session.read(cx);
520 let name = session.adapter().0;
521 Self::kvp_key(&name)
522 };
523 let settings = self.dap_store.update(cx, |this, cx| {
524 this.sync_adapter_options(session, cx);
525 });
526 let value = serde_json::to_string(&settings);
527
528 cx.background_executor()
529 .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
530 } else {
531 Task::ready(Result::Ok(()))
532 }
533 }
534
535 fn deserialize_exception_breakpoints(
536 &self,
537 adapter_name: DebugAdapterName,
538 cx: &mut Context<Self>,
539 ) -> anyhow::Result<()> {
540 let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
541 return Ok(());
542 };
543 let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
544 self.dap_store
545 .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
546
547 Ok(())
548 }
549
550 fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
551 let selected_ix = self.selected_ix;
552 let focus_handle = self.focus_handle.clone();
553 let supported_breakpoint_properties = self
554 .session
555 .as_ref()
556 .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
557 .unwrap_or_else(SupportedBreakpointProperties::empty);
558 let strip_mode = self.strip_mode;
559
560 uniform_list(
561 "breakpoint-list",
562 self.breakpoints.len(),
563 cx.processor(move |this, range: Range<usize>, _, _| {
564 range
565 .clone()
566 .zip(&mut this.breakpoints[range])
567 .map(|(ix, breakpoint)| {
568 breakpoint
569 .render(
570 strip_mode,
571 supported_breakpoint_properties,
572 ix,
573 Some(ix) == selected_ix,
574 focus_handle.clone(),
575 )
576 .into_any_element()
577 })
578 .collect()
579 }),
580 )
581 .track_scroll(self.scroll_handle.clone())
582 .flex_1()
583 }
584
585 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
586 div()
587 .occlude()
588 .id("breakpoint-list-vertical-scrollbar")
589 .on_mouse_move(cx.listener(|_, _, _, cx| {
590 cx.notify();
591 cx.stop_propagation()
592 }))
593 .on_hover(|_, _, cx| {
594 cx.stop_propagation();
595 })
596 .on_any_mouse_down(|_, _, cx| {
597 cx.stop_propagation();
598 })
599 .on_mouse_up(
600 MouseButton::Left,
601 cx.listener(|_, _, _, cx| {
602 cx.stop_propagation();
603 }),
604 )
605 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
606 cx.notify();
607 }))
608 .h_full()
609 .absolute()
610 .right_1()
611 .top_1()
612 .bottom_0()
613 .w(px(12.))
614 .cursor_default()
615 .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
616 }
617
618 pub(crate) fn render_control_strip(&self) -> AnyElement {
619 let selection_kind = self.selection_kind();
620 let focus_handle = self.focus_handle.clone();
621
622 let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
623 SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
624 SelectedBreakpointKind::Exception => {
625 "Exception Breakpoints cannot be removed from the breakpoint list"
626 }
627 SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
628 });
629
630 let toggle_label = selection_kind.map(|(_, is_enabled)| {
631 if is_enabled {
632 (
633 "Disable Breakpoint",
634 "Disable a breakpoint without removing it from the list",
635 )
636 } else {
637 ("Enable Breakpoint", "Re-enable a breakpoint")
638 }
639 });
640
641 h_flex()
642 .child(
643 IconButton::new(
644 "disable-breakpoint-breakpoint-list",
645 IconName::DebugDisabledBreakpoint,
646 )
647 .icon_size(IconSize::Small)
648 .when_some(toggle_label, |this, (label, meta)| {
649 this.tooltip({
650 let focus_handle = focus_handle.clone();
651 move |window, cx| {
652 Tooltip::with_meta_in(
653 label,
654 Some(&ToggleEnableBreakpoint),
655 meta,
656 &focus_handle,
657 window,
658 cx,
659 )
660 }
661 })
662 })
663 .disabled(selection_kind.is_none())
664 .on_click({
665 let focus_handle = focus_handle.clone();
666 move |_, window, cx| {
667 focus_handle.focus(window);
668 window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
669 }
670 }),
671 )
672 .child(
673 IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
674 .icon_size(IconSize::Small)
675 .when_some(remove_breakpoint_tooltip, |this, tooltip| {
676 this.tooltip({
677 let focus_handle = focus_handle.clone();
678 move |window, cx| {
679 Tooltip::with_meta_in(
680 "Remove Breakpoint",
681 Some(&UnsetBreakpoint),
682 tooltip,
683 &focus_handle,
684 window,
685 cx,
686 )
687 }
688 })
689 })
690 .disabled(
691 selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
692 )
693 .on_click({
694 let focus_handle = focus_handle.clone();
695 move |_, window, cx| {
696 focus_handle.focus(window);
697 window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
698 }
699 }),
700 )
701 .into_any_element()
702 }
703}
704
705impl Render for BreakpointList {
706 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
707 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
708 self.breakpoints.clear();
709 let weak = cx.weak_entity();
710 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
711 let relative_worktree_path = self
712 .worktree_store
713 .read(cx)
714 .find_worktree(&path, cx)
715 .and_then(|(worktree, relative_path)| {
716 worktree
717 .read(cx)
718 .is_visible()
719 .then(|| Path::new(worktree.read(cx).root_name()).join(relative_path))
720 });
721 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
722 let weak = weak.clone();
723 breakpoints.into_iter().filter_map(move |breakpoint| {
724 debug_assert_eq!(&path, &breakpoint.path);
725 let file_name = breakpoint.path.file_name()?;
726
727 let dir = relative_worktree_path
728 .clone()
729 .unwrap_or_else(|| PathBuf::from(&*breakpoint.path))
730 .parent()
731 .and_then(|parent| {
732 parent
733 .to_str()
734 .map(ToOwned::to_owned)
735 .map(SharedString::from)
736 });
737 let name = file_name
738 .to_str()
739 .map(ToOwned::to_owned)
740 .map(SharedString::from)?;
741 let weak = weak.clone();
742 let line = breakpoint.row + 1;
743 Some(BreakpointEntry {
744 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
745 name,
746 dir,
747 line,
748 breakpoint,
749 }),
750 weak,
751 })
752 })
753 });
754 let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
755 session
756 .read(cx)
757 .exception_breakpoints()
758 .map(|(data, is_enabled)| BreakpointEntry {
759 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
760 id: data.filter.clone(),
761 data: data.clone(),
762 is_enabled: *is_enabled,
763 }),
764 weak: weak.clone(),
765 })
766 });
767 let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
768 session
769 .read(cx)
770 .data_breakpoints()
771 .map(|state| BreakpointEntry {
772 kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
773 weak: weak.clone(),
774 })
775 });
776 self.breakpoints.extend(
777 breakpoints
778 .chain(data_breakpoints)
779 .chain(exception_breakpoints),
780 );
781
782 v_flex()
783 .id("breakpoint-list")
784 .key_context("BreakpointList")
785 .track_focus(&self.focus_handle)
786 .on_action(cx.listener(Self::select_next))
787 .on_action(cx.listener(Self::select_previous))
788 .on_action(cx.listener(Self::select_first))
789 .on_action(cx.listener(Self::select_last))
790 .on_action(cx.listener(Self::dismiss))
791 .on_action(cx.listener(Self::confirm))
792 .on_action(cx.listener(Self::toggle_enable_breakpoint))
793 .on_action(cx.listener(Self::unset_breakpoint))
794 .on_action(cx.listener(Self::next_breakpoint_property))
795 .on_action(cx.listener(Self::previous_breakpoint_property))
796 .size_full()
797 .pt_1()
798 .child(self.render_list(cx))
799 .child(self.render_vertical_scrollbar(cx))
800 .when_some(self.strip_mode, |this, _| {
801 this.child(Divider::horizontal().color(DividerColor::Border))
802 .child(
803 h_flex()
804 .p_1()
805 .rounded_sm()
806 .bg(cx.theme().colors().editor_background)
807 .border_1()
808 .when(
809 self.input.focus_handle(cx).contains_focused(window, cx),
810 |this| {
811 let colors = cx.theme().colors();
812
813 let border_color = if self.input.read(cx).read_only(cx) {
814 colors.border_disabled
815 } else {
816 colors.border_transparent
817 };
818
819 this.border_color(border_color)
820 },
821 )
822 .child(self.input.clone()),
823 )
824 })
825 }
826}
827
828#[derive(Clone, Debug)]
829struct LineBreakpoint {
830 name: SharedString,
831 dir: Option<SharedString>,
832 line: u32,
833 breakpoint: SourceBreakpoint,
834}
835
836impl LineBreakpoint {
837 fn render(
838 &mut self,
839 props: SupportedBreakpointProperties,
840 strip_mode: Option<ActiveBreakpointStripMode>,
841 ix: usize,
842 is_selected: bool,
843 focus_handle: FocusHandle,
844 weak: WeakEntity<BreakpointList>,
845 ) -> ListItem {
846 let icon_name = if self.breakpoint.state.is_enabled() {
847 IconName::DebugBreakpoint
848 } else {
849 IconName::DebugDisabledBreakpoint
850 };
851 let path = self.breakpoint.path.clone();
852 let row = self.breakpoint.row;
853 let is_enabled = self.breakpoint.state.is_enabled();
854
855 let indicator = div()
856 .id(SharedString::from(format!(
857 "breakpoint-ui-toggle-{:?}/{}:{}",
858 self.dir, self.name, self.line
859 )))
860 .child(
861 Icon::new(icon_name)
862 .color(Color::Debugger)
863 .size(IconSize::XSmall),
864 )
865 .tooltip({
866 let focus_handle = focus_handle.clone();
867 move |window, cx| {
868 Tooltip::for_action_in(
869 if is_enabled {
870 "Disable Breakpoint"
871 } else {
872 "Enable Breakpoint"
873 },
874 &ToggleEnableBreakpoint,
875 &focus_handle,
876 window,
877 cx,
878 )
879 }
880 })
881 .on_click({
882 let weak = weak.clone();
883 let path = path.clone();
884 move |_, _, cx| {
885 weak.update(cx, |breakpoint_list, cx| {
886 breakpoint_list.edit_line_breakpoint(
887 path.clone(),
888 row,
889 BreakpointEditAction::InvertState,
890 cx,
891 );
892 })
893 .ok();
894 }
895 })
896 .on_mouse_down(MouseButton::Left, move |_, _, _| {});
897
898 ListItem::new(SharedString::from(format!(
899 "breakpoint-ui-item-{:?}/{}:{}",
900 self.dir, self.name, self.line
901 )))
902 .toggle_state(is_selected)
903 .inset(true)
904 .on_click({
905 let weak = weak.clone();
906 move |_, window, cx| {
907 weak.update(cx, |breakpoint_list, cx| {
908 breakpoint_list.select_ix(Some(ix), window, cx);
909 })
910 .ok();
911 }
912 })
913 .on_secondary_mouse_down(|_, _, cx| {
914 cx.stop_propagation();
915 })
916 .start_slot(indicator)
917 .child(
918 h_flex()
919 .id(SharedString::from(format!(
920 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
921 self.dir, self.name, self.line
922 )))
923 .w_full()
924 .gap_1()
925 .min_h(rems_from_px(26.))
926 .justify_between()
927 .on_click({
928 let weak = weak.clone();
929 move |_, window, cx| {
930 weak.update(cx, |breakpoint_list, cx| {
931 breakpoint_list.select_ix(Some(ix), window, cx);
932 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
933 })
934 .ok();
935 }
936 })
937 .child(
938 h_flex()
939 .id("label-container")
940 .gap_0p5()
941 .child(
942 Label::new(format!("{}:{}", self.name, self.line))
943 .size(LabelSize::Small)
944 .line_height_style(ui::LineHeightStyle::UiLabel),
945 )
946 .children(self.dir.as_ref().and_then(|dir| {
947 let path_without_root = Path::new(dir.as_ref())
948 .components()
949 .skip(1)
950 .collect::<PathBuf>();
951 path_without_root.components().next()?;
952 Some(
953 Label::new(path_without_root.to_string_lossy().into_owned())
954 .color(Color::Muted)
955 .size(LabelSize::Small)
956 .line_height_style(ui::LineHeightStyle::UiLabel)
957 .truncate(),
958 )
959 }))
960 .when_some(self.dir.as_ref(), |this, parent_dir| {
961 this.tooltip(Tooltip::text(format!(
962 "Worktree parent path: {parent_dir}"
963 )))
964 }),
965 )
966 .child(BreakpointOptionsStrip {
967 props,
968 breakpoint: BreakpointEntry {
969 kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
970 weak: weak,
971 },
972 is_selected,
973 focus_handle,
974 strip_mode,
975 index: ix,
976 }),
977 )
978 }
979}
980
981#[derive(Clone, Debug)]
982struct ExceptionBreakpoint {
983 id: String,
984 data: ExceptionBreakpointsFilter,
985 is_enabled: bool,
986}
987
988#[derive(Clone, Debug)]
989struct DataBreakpoint(project::debugger::session::DataBreakpointState);
990
991impl DataBreakpoint {
992 fn render(
993 &self,
994 props: SupportedBreakpointProperties,
995 strip_mode: Option<ActiveBreakpointStripMode>,
996 ix: usize,
997 is_selected: bool,
998 focus_handle: FocusHandle,
999 list: WeakEntity<BreakpointList>,
1000 ) -> ListItem {
1001 let color = if self.0.is_enabled {
1002 Color::Debugger
1003 } else {
1004 Color::Muted
1005 };
1006 let is_enabled = self.0.is_enabled;
1007 let id = self.0.dap.data_id.clone();
1008
1009 ListItem::new(SharedString::from(format!(
1010 "data-breakpoint-ui-item-{}",
1011 self.0.dap.data_id
1012 )))
1013 .toggle_state(is_selected)
1014 .inset(true)
1015 .start_slot(
1016 div()
1017 .id(SharedString::from(format!(
1018 "data-breakpoint-ui-item-{}-click-handler",
1019 self.0.dap.data_id
1020 )))
1021 .child(
1022 Icon::new(IconName::Binary)
1023 .color(color)
1024 .size(IconSize::Small),
1025 )
1026 .tooltip({
1027 let focus_handle = focus_handle.clone();
1028 move |window, cx| {
1029 Tooltip::for_action_in(
1030 if is_enabled {
1031 "Disable Data Breakpoint"
1032 } else {
1033 "Enable Data Breakpoint"
1034 },
1035 &ToggleEnableBreakpoint,
1036 &focus_handle,
1037 window,
1038 cx,
1039 )
1040 }
1041 })
1042 .on_click({
1043 let list = list.clone();
1044 move |_, _, cx| {
1045 list.update(cx, |this, cx| {
1046 this.toggle_data_breakpoint(&id, cx);
1047 })
1048 .ok();
1049 }
1050 }),
1051 )
1052 .child(
1053 h_flex()
1054 .w_full()
1055 .gap_1()
1056 .min_h(rems_from_px(26.))
1057 .justify_between()
1058 .child(
1059 v_flex()
1060 .py_1()
1061 .gap_1()
1062 .justify_center()
1063 .id(("data-breakpoint-label", ix))
1064 .child(
1065 Label::new(self.0.context.human_readable_label())
1066 .size(LabelSize::Small)
1067 .line_height_style(ui::LineHeightStyle::UiLabel),
1068 ),
1069 )
1070 .child(BreakpointOptionsStrip {
1071 props,
1072 breakpoint: BreakpointEntry {
1073 kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1074 weak: list,
1075 },
1076 is_selected,
1077 focus_handle,
1078 strip_mode,
1079 index: ix,
1080 }),
1081 )
1082 }
1083}
1084
1085impl ExceptionBreakpoint {
1086 fn render(
1087 &mut self,
1088 props: SupportedBreakpointProperties,
1089 strip_mode: Option<ActiveBreakpointStripMode>,
1090 ix: usize,
1091 is_selected: bool,
1092 focus_handle: FocusHandle,
1093 list: WeakEntity<BreakpointList>,
1094 ) -> ListItem {
1095 let color = if self.is_enabled {
1096 Color::Debugger
1097 } else {
1098 Color::Muted
1099 };
1100 let id = SharedString::from(&self.id);
1101 let is_enabled = self.is_enabled;
1102 let weak = list.clone();
1103
1104 ListItem::new(SharedString::from(format!(
1105 "exception-breakpoint-ui-item-{}",
1106 self.id
1107 )))
1108 .toggle_state(is_selected)
1109 .inset(true)
1110 .on_click({
1111 let list = list.clone();
1112 move |_, window, cx| {
1113 list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1114 .ok();
1115 }
1116 })
1117 .on_secondary_mouse_down(|_, _, cx| {
1118 cx.stop_propagation();
1119 })
1120 .start_slot(
1121 div()
1122 .id(SharedString::from(format!(
1123 "exception-breakpoint-ui-item-{}-click-handler",
1124 self.id
1125 )))
1126 .child(
1127 Icon::new(IconName::Flame)
1128 .color(color)
1129 .size(IconSize::Small),
1130 )
1131 .tooltip({
1132 let focus_handle = focus_handle.clone();
1133 move |window, cx| {
1134 Tooltip::for_action_in(
1135 if is_enabled {
1136 "Disable Exception Breakpoint"
1137 } else {
1138 "Enable Exception Breakpoint"
1139 },
1140 &ToggleEnableBreakpoint,
1141 &focus_handle,
1142 window,
1143 cx,
1144 )
1145 }
1146 })
1147 .on_click({
1148 let list = list.clone();
1149 move |_, _, cx| {
1150 list.update(cx, |this, cx| {
1151 this.toggle_exception_breakpoint(&id, cx);
1152 })
1153 .ok();
1154 }
1155 }),
1156 )
1157 .child(
1158 h_flex()
1159 .w_full()
1160 .gap_1()
1161 .min_h(rems_from_px(26.))
1162 .justify_between()
1163 .child(
1164 v_flex()
1165 .py_1()
1166 .gap_1()
1167 .justify_center()
1168 .id(("exception-breakpoint-label", ix))
1169 .child(
1170 Label::new(self.data.label.clone())
1171 .size(LabelSize::Small)
1172 .line_height_style(ui::LineHeightStyle::UiLabel),
1173 )
1174 .when_some(self.data.description.clone(), |el, description| {
1175 el.tooltip(Tooltip::text(description))
1176 }),
1177 )
1178 .child(BreakpointOptionsStrip {
1179 props,
1180 breakpoint: BreakpointEntry {
1181 kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1182 weak: weak,
1183 },
1184 is_selected,
1185 focus_handle,
1186 strip_mode,
1187 index: ix,
1188 }),
1189 )
1190 }
1191}
1192#[derive(Clone, Debug)]
1193enum BreakpointEntryKind {
1194 LineBreakpoint(LineBreakpoint),
1195 ExceptionBreakpoint(ExceptionBreakpoint),
1196 DataBreakpoint(DataBreakpoint),
1197}
1198
1199#[derive(Clone, Debug)]
1200struct BreakpointEntry {
1201 kind: BreakpointEntryKind,
1202 weak: WeakEntity<BreakpointList>,
1203}
1204
1205impl BreakpointEntry {
1206 fn render(
1207 &mut self,
1208 strip_mode: Option<ActiveBreakpointStripMode>,
1209 props: SupportedBreakpointProperties,
1210 ix: usize,
1211 is_selected: bool,
1212 focus_handle: FocusHandle,
1213 ) -> ListItem {
1214 match &mut self.kind {
1215 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1216 props,
1217 strip_mode,
1218 ix,
1219 is_selected,
1220 focus_handle,
1221 self.weak.clone(),
1222 ),
1223 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1224 .render(
1225 props.for_exception_breakpoints(),
1226 strip_mode,
1227 ix,
1228 is_selected,
1229 focus_handle,
1230 self.weak.clone(),
1231 ),
1232 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1233 props.for_data_breakpoints(),
1234 strip_mode,
1235 ix,
1236 is_selected,
1237 focus_handle,
1238 self.weak.clone(),
1239 ),
1240 }
1241 }
1242
1243 fn id(&self) -> SharedString {
1244 match &self.kind {
1245 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1246 "source-breakpoint-control-strip-{:?}:{}",
1247 line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1248 )
1249 .into(),
1250 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1251 "exception-breakpoint-control-strip--{}",
1252 exception_breakpoint.id
1253 )
1254 .into(),
1255 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1256 "data-breakpoint-control-strip--{}",
1257 data_breakpoint.0.dap.data_id
1258 )
1259 .into(),
1260 }
1261 }
1262
1263 fn has_log(&self) -> bool {
1264 match &self.kind {
1265 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1266 line_breakpoint.breakpoint.message.is_some()
1267 }
1268 _ => false,
1269 }
1270 }
1271
1272 fn has_condition(&self) -> bool {
1273 match &self.kind {
1274 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1275 line_breakpoint.breakpoint.condition.is_some()
1276 }
1277 // We don't support conditions on exception/data breakpoints
1278 _ => false,
1279 }
1280 }
1281
1282 fn has_hit_condition(&self) -> bool {
1283 match &self.kind {
1284 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1285 line_breakpoint.breakpoint.hit_condition.is_some()
1286 }
1287 _ => false,
1288 }
1289 }
1290}
1291
1292bitflags::bitflags! {
1293 #[derive(Clone, Copy)]
1294 pub struct SupportedBreakpointProperties: u32 {
1295 const LOG = 1 << 0;
1296 const CONDITION = 1 << 1;
1297 const HIT_CONDITION = 1 << 2;
1298 // Conditions for exceptions can be set only when exception filters are supported.
1299 const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1300 }
1301}
1302
1303impl From<&Capabilities> for SupportedBreakpointProperties {
1304 fn from(caps: &Capabilities) -> Self {
1305 let mut this = Self::empty();
1306 for (prop, offset) in [
1307 (caps.supports_log_points, Self::LOG),
1308 (caps.supports_conditional_breakpoints, Self::CONDITION),
1309 (
1310 caps.supports_hit_conditional_breakpoints,
1311 Self::HIT_CONDITION,
1312 ),
1313 (
1314 caps.supports_exception_options,
1315 Self::EXCEPTION_FILTER_OPTIONS,
1316 ),
1317 ] {
1318 if prop.unwrap_or_default() {
1319 this.insert(offset);
1320 }
1321 }
1322 this
1323 }
1324}
1325
1326impl SupportedBreakpointProperties {
1327 fn for_exception_breakpoints(self) -> Self {
1328 // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1329 Self::empty()
1330 }
1331 fn for_data_breakpoints(self) -> Self {
1332 // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1333 Self::empty()
1334 }
1335}
1336#[derive(IntoElement)]
1337struct BreakpointOptionsStrip {
1338 props: SupportedBreakpointProperties,
1339 breakpoint: BreakpointEntry,
1340 is_selected: bool,
1341 focus_handle: FocusHandle,
1342 strip_mode: Option<ActiveBreakpointStripMode>,
1343 index: usize,
1344}
1345
1346impl BreakpointOptionsStrip {
1347 fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1348 self.is_selected && self.strip_mode == Some(expected_mode)
1349 }
1350
1351 fn on_click_callback(
1352 &self,
1353 mode: ActiveBreakpointStripMode,
1354 ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1355 let list = self.breakpoint.weak.clone();
1356 let ix = self.index;
1357 move |_, window, cx| {
1358 list.update(cx, |this, cx| {
1359 if this.strip_mode != Some(mode) {
1360 this.set_active_breakpoint_property(mode, window, cx);
1361 } else if this.selected_ix == Some(ix) {
1362 this.strip_mode.take();
1363 } else {
1364 cx.propagate();
1365 }
1366 })
1367 .ok();
1368 }
1369 }
1370
1371 fn add_focus_styles(
1372 &self,
1373 kind: ActiveBreakpointStripMode,
1374 available: bool,
1375 window: &Window,
1376 cx: &App,
1377 ) -> impl Fn(Div) -> Div {
1378 move |this: Div| {
1379 // Avoid layout shifts in case there's no colored border
1380 let this = this.border_1().rounded_sm();
1381 let color = cx.theme().colors();
1382
1383 if self.is_selected && self.strip_mode == Some(kind) {
1384 if self.focus_handle.is_focused(window) {
1385 this.bg(color.editor_background)
1386 .border_color(color.border_focused)
1387 } else {
1388 this.border_color(color.border)
1389 }
1390 } else if !available {
1391 this.border_color(color.border_transparent)
1392 } else {
1393 this
1394 }
1395 }
1396 }
1397}
1398
1399impl RenderOnce for BreakpointOptionsStrip {
1400 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1401 let id = self.breakpoint.id();
1402 let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1403 let supports_condition = self
1404 .props
1405 .contains(SupportedBreakpointProperties::CONDITION);
1406 let supports_hit_condition = self
1407 .props
1408 .contains(SupportedBreakpointProperties::HIT_CONDITION);
1409 let has_logs = self.breakpoint.has_log();
1410 let has_condition = self.breakpoint.has_condition();
1411 let has_hit_condition = self.breakpoint.has_hit_condition();
1412 let style_for_toggle = |mode, is_enabled| {
1413 if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1414 ui::ButtonStyle::Filled
1415 } else {
1416 ui::ButtonStyle::Subtle
1417 }
1418 };
1419 let color_for_toggle = |is_enabled| {
1420 if is_enabled {
1421 Color::Default
1422 } else {
1423 Color::Muted
1424 }
1425 };
1426
1427 h_flex()
1428 .gap_px()
1429 .mr_3() // Space to avoid overlapping with the scrollbar
1430 .child(
1431 div()
1432 .map(self.add_focus_styles(
1433 ActiveBreakpointStripMode::Log,
1434 supports_logs,
1435 window,
1436 cx,
1437 ))
1438 .child(
1439 IconButton::new(
1440 SharedString::from(format!("{id}-log-toggle")),
1441 IconName::Notepad,
1442 )
1443 .shape(ui::IconButtonShape::Square)
1444 .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
1445 .icon_size(IconSize::Small)
1446 .icon_color(color_for_toggle(has_logs))
1447 .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
1448 .disabled(!supports_logs)
1449 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
1450 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
1451 .tooltip(|window, cx| {
1452 Tooltip::with_meta(
1453 "Set Log Message",
1454 None,
1455 "Set log message to display (instead of stopping) when a breakpoint is hit.",
1456 window,
1457 cx,
1458 )
1459 }),
1460 )
1461 .when(!has_logs && !self.is_selected, |this| this.invisible()),
1462 )
1463 .child(
1464 div()
1465 .map(self.add_focus_styles(
1466 ActiveBreakpointStripMode::Condition,
1467 supports_condition,
1468 window,
1469 cx,
1470 ))
1471 .child(
1472 IconButton::new(
1473 SharedString::from(format!("{id}-condition-toggle")),
1474 IconName::SplitAlt,
1475 )
1476 .shape(ui::IconButtonShape::Square)
1477 .style(style_for_toggle(
1478 ActiveBreakpointStripMode::Condition,
1479 has_condition,
1480 ))
1481 .icon_size(IconSize::Small)
1482 .icon_color(color_for_toggle(has_condition))
1483 .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1484 .disabled(!supports_condition)
1485 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
1486 .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
1487 .tooltip(|window, cx| {
1488 Tooltip::with_meta(
1489 "Set Condition",
1490 None,
1491 "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
1492 window,
1493 cx,
1494 )
1495 }),
1496 )
1497 .when(!has_condition && !self.is_selected, |this| this.invisible()),
1498 )
1499 .child(
1500 div()
1501 .map(self.add_focus_styles(
1502 ActiveBreakpointStripMode::HitCondition,
1503 supports_hit_condition,
1504 window,
1505 cx,
1506 ))
1507 .child(
1508 IconButton::new(
1509 SharedString::from(format!("{id}-hit-condition-toggle")),
1510 IconName::ArrowDown10,
1511 )
1512 .style(style_for_toggle(
1513 ActiveBreakpointStripMode::HitCondition,
1514 has_hit_condition,
1515 ))
1516 .shape(ui::IconButtonShape::Square)
1517 .icon_size(IconSize::Small)
1518 .icon_color(color_for_toggle(has_hit_condition))
1519 .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
1520 .disabled(!supports_hit_condition)
1521 .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
1522 .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
1523 .tooltip(|window, cx| {
1524 Tooltip::with_meta(
1525 "Set Hit Condition",
1526 None,
1527 "Set expression that controls how many hits of the breakpoint are ignored.",
1528 window,
1529 cx,
1530 )
1531 }),
1532 )
1533 .when(!has_hit_condition && !self.is_selected, |this| {
1534 this.invisible()
1535 }),
1536 )
1537 }
1538}