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