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