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