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 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);
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);
368 } else {
369 handle.focus(window);
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 cx.background_executor()
524 .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
525 } else {
526 Task::ready(Result::Ok(()))
527 }
528 }
529
530 fn deserialize_exception_breakpoints(
531 &self,
532 adapter_name: DebugAdapterName,
533 cx: &mut Context<Self>,
534 ) -> anyhow::Result<()> {
535 let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
536 return Ok(());
537 };
538 let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
539 self.dap_store
540 .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
541
542 Ok(())
543 }
544
545 fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
546 let selected_ix = self.selected_ix;
547 let focus_handle = self.focus_handle.clone();
548 let supported_breakpoint_properties = self
549 .session
550 .as_ref()
551 .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
552 .unwrap_or_else(SupportedBreakpointProperties::all);
553 let strip_mode = self.strip_mode;
554
555 uniform_list(
556 "breakpoint-list",
557 self.breakpoints.len(),
558 cx.processor(move |this, range: Range<usize>, _, _| {
559 range
560 .clone()
561 .zip(&mut this.breakpoints[range])
562 .map(|(ix, breakpoint)| {
563 breakpoint
564 .render(
565 strip_mode,
566 supported_breakpoint_properties,
567 ix,
568 Some(ix) == selected_ix,
569 focus_handle.clone(),
570 )
571 .into_any_element()
572 })
573 .collect()
574 }),
575 )
576 .with_horizontal_sizing_behavior(gpui::ListHorizontalSizingBehavior::Unconstrained)
577 .with_width_from_item(self.max_width_index)
578 .track_scroll(self.scroll_handle.clone())
579 .flex_1()
580 }
581
582 pub(crate) fn render_control_strip(&self) -> AnyElement {
583 let selection_kind = self.selection_kind();
584 let focus_handle = self.focus_handle.clone();
585
586 let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
587 SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
588 SelectedBreakpointKind::Exception => {
589 "Exception Breakpoints cannot be removed from the breakpoint list"
590 }
591 SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
592 });
593
594 let toggle_label = selection_kind.map(|(_, is_enabled)| {
595 if is_enabled {
596 (
597 "Disable Breakpoint",
598 "Disable a breakpoint without removing it from the list",
599 )
600 } else {
601 ("Enable Breakpoint", "Re-enable a breakpoint")
602 }
603 });
604
605 h_flex()
606 .child(
607 IconButton::new(
608 "disable-breakpoint-breakpoint-list",
609 IconName::DebugDisabledBreakpoint,
610 )
611 .icon_size(IconSize::Small)
612 .when_some(toggle_label, |this, (label, meta)| {
613 this.tooltip({
614 let focus_handle = focus_handle.clone();
615 move |_window, cx| {
616 Tooltip::with_meta_in(
617 label,
618 Some(&ToggleEnableBreakpoint),
619 meta,
620 &focus_handle,
621 cx,
622 )
623 }
624 })
625 })
626 .disabled(selection_kind.is_none())
627 .on_click({
628 let focus_handle = focus_handle.clone();
629 move |_, window, cx| {
630 focus_handle.focus(window);
631 window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
632 }
633 }),
634 )
635 .child(
636 IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
637 .icon_size(IconSize::Small)
638 .when_some(remove_breakpoint_tooltip, |this, tooltip| {
639 this.tooltip({
640 let focus_handle = focus_handle.clone();
641 move |_window, cx| {
642 Tooltip::with_meta_in(
643 "Remove Breakpoint",
644 Some(&UnsetBreakpoint),
645 tooltip,
646 &focus_handle,
647 cx,
648 )
649 }
650 })
651 })
652 .disabled(
653 selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
654 )
655 .on_click({
656 move |_, window, cx| {
657 focus_handle.focus(window);
658 window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
659 }
660 }),
661 )
662 .into_any_element()
663 }
664}
665
666impl Render for BreakpointList {
667 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
668 let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
669 self.breakpoints.clear();
670 let path_style = self.worktree_store.read(cx).path_style();
671 let weak = cx.weak_entity();
672 let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
673 let relative_worktree_path = self
674 .worktree_store
675 .read(cx)
676 .find_worktree(&path, cx)
677 .and_then(|(worktree, relative_path)| {
678 worktree
679 .read(cx)
680 .is_visible()
681 .then(|| worktree.read(cx).root_name().join(&relative_path))
682 });
683 breakpoints.sort_by_key(|breakpoint| breakpoint.row);
684 let weak = weak.clone();
685 breakpoints.into_iter().filter_map(move |breakpoint| {
686 debug_assert_eq!(&path, &breakpoint.path);
687 let file_name = breakpoint.path.file_name()?;
688 let breakpoint_path = RelPath::new(&breakpoint.path, path_style).ok();
689
690 let dir = relative_worktree_path
691 .as_deref()
692 .or(breakpoint_path.as_deref())?
693 .parent()
694 .map(|parent| SharedString::from(parent.display(path_style).to_string()));
695 let name = file_name
696 .to_str()
697 .map(ToOwned::to_owned)
698 .map(SharedString::from)?;
699 let weak = weak.clone();
700 let line = breakpoint.row + 1;
701 Some(BreakpointEntry {
702 kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
703 name,
704 dir,
705 line,
706 breakpoint,
707 }),
708 weak,
709 })
710 })
711 });
712 let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
713 session
714 .read(cx)
715 .exception_breakpoints()
716 .map(|(data, is_enabled)| BreakpointEntry {
717 kind: BreakpointEntryKind::ExceptionBreakpoint(ExceptionBreakpoint {
718 id: data.filter.clone(),
719 data: data.clone(),
720 is_enabled: *is_enabled,
721 }),
722 weak: weak.clone(),
723 })
724 });
725 let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
726 session
727 .read(cx)
728 .data_breakpoints()
729 .map(|state| BreakpointEntry {
730 kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
731 weak: weak.clone(),
732 })
733 });
734 self.breakpoints.extend(
735 breakpoints
736 .chain(data_breakpoints)
737 .chain(exception_breakpoints),
738 );
739
740 let text_pixels = ui::TextSize::Default.pixels(cx).to_f64() as f32;
741
742 self.max_width_index = self
743 .breakpoints
744 .iter()
745 .map(|entry| match &entry.kind {
746 BreakpointEntryKind::LineBreakpoint(line_bp) => {
747 let name_and_line = format!("{}:{}", line_bp.name, line_bp.line);
748 let dir_len = line_bp.dir.as_ref().map(|d| d.len()).unwrap_or(0);
749 (name_and_line.len() + dir_len) as f32 * text_pixels
750 }
751 BreakpointEntryKind::ExceptionBreakpoint(exc_bp) => {
752 exc_bp.data.label.len() as f32 * text_pixels
753 }
754 BreakpointEntryKind::DataBreakpoint(data_bp) => {
755 data_bp.0.context.human_readable_label().len() as f32 * text_pixels
756 }
757 })
758 .position_max_by(|left, right| left.total_cmp(right));
759
760 v_flex()
761 .id("breakpoint-list")
762 .key_context("BreakpointList")
763 .track_focus(&self.focus_handle)
764 .on_action(cx.listener(Self::select_next))
765 .on_action(cx.listener(Self::select_previous))
766 .on_action(cx.listener(Self::select_first))
767 .on_action(cx.listener(Self::select_last))
768 .on_action(cx.listener(Self::dismiss))
769 .on_action(cx.listener(Self::confirm))
770 .on_action(cx.listener(Self::toggle_enable_breakpoint))
771 .on_action(cx.listener(Self::unset_breakpoint))
772 .on_action(cx.listener(Self::next_breakpoint_property))
773 .on_action(cx.listener(Self::previous_breakpoint_property))
774 .size_full()
775 .pt_1()
776 .child(self.render_list(cx))
777 .custom_scrollbars(
778 ui::Scrollbars::new(ScrollAxes::Both)
779 .tracked_scroll_handle(self.scroll_handle.clone())
780 .with_track_along(ScrollAxes::Both, cx.theme().colors().panel_background)
781 .tracked_entity(cx.entity_id()),
782 window,
783 cx,
784 )
785 .when_some(self.strip_mode, |this, _| {
786 this.child(Divider::horizontal().color(DividerColor::Border))
787 .child(
788 h_flex()
789 .p_1()
790 .rounded_sm()
791 .bg(cx.theme().colors().editor_background)
792 .border_1()
793 .when(
794 self.input.focus_handle(cx).contains_focused(window, cx),
795 |this| {
796 let colors = cx.theme().colors();
797
798 let border_color = if self.input.read(cx).read_only(cx) {
799 colors.border_disabled
800 } else {
801 colors.border_transparent
802 };
803
804 this.border_color(border_color)
805 },
806 )
807 .child(self.input.clone()),
808 )
809 })
810 }
811}
812
813#[derive(Clone, Debug)]
814struct LineBreakpoint {
815 name: SharedString,
816 dir: Option<SharedString>,
817 line: u32,
818 breakpoint: SourceBreakpoint,
819}
820
821impl LineBreakpoint {
822 fn render(
823 &mut self,
824 props: SupportedBreakpointProperties,
825 strip_mode: Option<ActiveBreakpointStripMode>,
826 ix: usize,
827 is_selected: bool,
828 focus_handle: FocusHandle,
829 weak: WeakEntity<BreakpointList>,
830 ) -> ListItem {
831 let icon_name = if self.breakpoint.state.is_enabled() {
832 IconName::DebugBreakpoint
833 } else {
834 IconName::DebugDisabledBreakpoint
835 };
836 let path = self.breakpoint.path.clone();
837 let row = self.breakpoint.row;
838 let is_enabled = self.breakpoint.state.is_enabled();
839
840 let indicator = div()
841 .id(SharedString::from(format!(
842 "breakpoint-ui-toggle-{:?}/{}:{}",
843 self.dir, self.name, self.line
844 )))
845 .child(
846 Icon::new(icon_name)
847 .color(Color::Debugger)
848 .size(IconSize::XSmall),
849 )
850 .tooltip({
851 let focus_handle = focus_handle.clone();
852 move |_window, cx| {
853 Tooltip::for_action_in(
854 if is_enabled {
855 "Disable Breakpoint"
856 } else {
857 "Enable Breakpoint"
858 },
859 &ToggleEnableBreakpoint,
860 &focus_handle,
861 cx,
862 )
863 }
864 })
865 .on_click({
866 let weak = weak.clone();
867 let path = path.clone();
868 move |_, _, cx| {
869 weak.update(cx, |breakpoint_list, cx| {
870 breakpoint_list.edit_line_breakpoint(
871 path.clone(),
872 row,
873 BreakpointEditAction::InvertState,
874 cx,
875 );
876 })
877 .ok();
878 }
879 })
880 .on_mouse_down(MouseButton::Left, move |_, _, _| {});
881
882 ListItem::new(SharedString::from(format!(
883 "breakpoint-ui-item-{:?}/{}:{}",
884 self.dir, self.name, self.line
885 )))
886 .toggle_state(is_selected)
887 .inset(true)
888 .on_click({
889 let weak = weak.clone();
890 move |_, window, cx| {
891 weak.update(cx, |breakpoint_list, cx| {
892 breakpoint_list.select_ix(Some(ix), window, cx);
893 })
894 .ok();
895 }
896 })
897 .on_secondary_mouse_down(|_, _, cx| {
898 cx.stop_propagation();
899 })
900 .start_slot(indicator)
901 .child(
902 h_flex()
903 .id(SharedString::from(format!(
904 "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
905 self.dir, self.name, self.line
906 )))
907 .w_full()
908 .gap_1()
909 .min_h(rems_from_px(26.))
910 .justify_between()
911 .on_click({
912 let weak = weak.clone();
913 move |_, window, cx| {
914 weak.update(cx, |breakpoint_list, cx| {
915 breakpoint_list.select_ix(Some(ix), window, cx);
916 breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
917 })
918 .ok();
919 }
920 })
921 .child(
922 h_flex()
923 .id("label-container")
924 .gap_0p5()
925 .child(
926 Label::new(format!("{}:{}", self.name, self.line))
927 .size(LabelSize::Small)
928 .line_height_style(ui::LineHeightStyle::UiLabel),
929 )
930 .children(self.dir.as_ref().and_then(|dir| {
931 let path_without_root = Path::new(dir.as_ref())
932 .components()
933 .skip(1)
934 .collect::<PathBuf>();
935 path_without_root.components().next()?;
936 Some(
937 Label::new(path_without_root.to_string_lossy().into_owned())
938 .color(Color::Muted)
939 .size(LabelSize::Small)
940 .line_height_style(ui::LineHeightStyle::UiLabel)
941 .truncate(),
942 )
943 }))
944 .when_some(self.dir.as_ref(), |this, parent_dir| {
945 this.tooltip(Tooltip::text(format!(
946 "Worktree parent path: {parent_dir}"
947 )))
948 }),
949 )
950 .child(BreakpointOptionsStrip {
951 props,
952 breakpoint: BreakpointEntry {
953 kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
954 weak,
955 },
956 is_selected,
957 focus_handle,
958 strip_mode,
959 index: ix,
960 }),
961 )
962 }
963}
964
965#[derive(Clone, Debug)]
966struct ExceptionBreakpoint {
967 id: String,
968 data: ExceptionBreakpointsFilter,
969 is_enabled: bool,
970}
971
972#[derive(Clone, Debug)]
973struct DataBreakpoint(project::debugger::session::DataBreakpointState);
974
975impl DataBreakpoint {
976 fn render(
977 &self,
978 props: SupportedBreakpointProperties,
979 strip_mode: Option<ActiveBreakpointStripMode>,
980 ix: usize,
981 is_selected: bool,
982 focus_handle: FocusHandle,
983 list: WeakEntity<BreakpointList>,
984 ) -> ListItem {
985 let color = if self.0.is_enabled {
986 Color::Debugger
987 } else {
988 Color::Muted
989 };
990 let is_enabled = self.0.is_enabled;
991 let id = self.0.dap.data_id.clone();
992
993 ListItem::new(SharedString::from(format!(
994 "data-breakpoint-ui-item-{}",
995 self.0.dap.data_id
996 )))
997 .toggle_state(is_selected)
998 .inset(true)
999 .start_slot(
1000 div()
1001 .id(SharedString::from(format!(
1002 "data-breakpoint-ui-item-{}-click-handler",
1003 self.0.dap.data_id
1004 )))
1005 .child(
1006 Icon::new(IconName::Binary)
1007 .color(color)
1008 .size(IconSize::Small),
1009 )
1010 .tooltip({
1011 let focus_handle = focus_handle.clone();
1012 move |_window, cx| {
1013 Tooltip::for_action_in(
1014 if is_enabled {
1015 "Disable Data Breakpoint"
1016 } else {
1017 "Enable Data Breakpoint"
1018 },
1019 &ToggleEnableBreakpoint,
1020 &focus_handle,
1021 cx,
1022 )
1023 }
1024 })
1025 .on_click({
1026 let list = list.clone();
1027 move |_, _, cx| {
1028 list.update(cx, |this, cx| {
1029 this.toggle_data_breakpoint(&id, cx);
1030 })
1031 .ok();
1032 }
1033 }),
1034 )
1035 .child(
1036 h_flex()
1037 .w_full()
1038 .gap_1()
1039 .min_h(rems_from_px(26.))
1040 .justify_between()
1041 .child(
1042 v_flex()
1043 .py_1()
1044 .gap_1()
1045 .justify_center()
1046 .id(("data-breakpoint-label", ix))
1047 .child(
1048 Label::new(self.0.context.human_readable_label())
1049 .size(LabelSize::Small)
1050 .line_height_style(ui::LineHeightStyle::UiLabel),
1051 ),
1052 )
1053 .child(BreakpointOptionsStrip {
1054 props,
1055 breakpoint: BreakpointEntry {
1056 kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
1057 weak: list,
1058 },
1059 is_selected,
1060 focus_handle,
1061 strip_mode,
1062 index: ix,
1063 }),
1064 )
1065 }
1066}
1067
1068impl ExceptionBreakpoint {
1069 fn render(
1070 &mut self,
1071 props: SupportedBreakpointProperties,
1072 strip_mode: Option<ActiveBreakpointStripMode>,
1073 ix: usize,
1074 is_selected: bool,
1075 focus_handle: FocusHandle,
1076 list: WeakEntity<BreakpointList>,
1077 ) -> ListItem {
1078 let color = if self.is_enabled {
1079 Color::Debugger
1080 } else {
1081 Color::Muted
1082 };
1083 let id = SharedString::from(&self.id);
1084 let is_enabled = self.is_enabled;
1085 let weak = list.clone();
1086
1087 ListItem::new(SharedString::from(format!(
1088 "exception-breakpoint-ui-item-{}",
1089 self.id
1090 )))
1091 .toggle_state(is_selected)
1092 .inset(true)
1093 .on_click({
1094 let list = list.clone();
1095 move |_, window, cx| {
1096 list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
1097 .ok();
1098 }
1099 })
1100 .on_secondary_mouse_down(|_, _, cx| {
1101 cx.stop_propagation();
1102 })
1103 .start_slot(
1104 div()
1105 .id(SharedString::from(format!(
1106 "exception-breakpoint-ui-item-{}-click-handler",
1107 self.id
1108 )))
1109 .child(
1110 Icon::new(IconName::Flame)
1111 .color(color)
1112 .size(IconSize::Small),
1113 )
1114 .tooltip({
1115 let focus_handle = focus_handle.clone();
1116 move |_window, cx| {
1117 Tooltip::for_action_in(
1118 if is_enabled {
1119 "Disable Exception Breakpoint"
1120 } else {
1121 "Enable Exception Breakpoint"
1122 },
1123 &ToggleEnableBreakpoint,
1124 &focus_handle,
1125 cx,
1126 )
1127 }
1128 })
1129 .on_click({
1130 move |_, _, cx| {
1131 list.update(cx, |this, cx| {
1132 this.toggle_exception_breakpoint(&id, cx);
1133 })
1134 .ok();
1135 }
1136 }),
1137 )
1138 .child(
1139 h_flex()
1140 .w_full()
1141 .gap_1()
1142 .min_h(rems_from_px(26.))
1143 .justify_between()
1144 .child(
1145 v_flex()
1146 .py_1()
1147 .gap_1()
1148 .justify_center()
1149 .id(("exception-breakpoint-label", ix))
1150 .child(
1151 Label::new(self.data.label.clone())
1152 .size(LabelSize::Small)
1153 .line_height_style(ui::LineHeightStyle::UiLabel),
1154 )
1155 .when_some(self.data.description.clone(), |el, description| {
1156 el.tooltip(Tooltip::text(description))
1157 }),
1158 )
1159 .child(BreakpointOptionsStrip {
1160 props,
1161 breakpoint: BreakpointEntry {
1162 kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
1163 weak,
1164 },
1165 is_selected,
1166 focus_handle,
1167 strip_mode,
1168 index: ix,
1169 }),
1170 )
1171 }
1172}
1173#[derive(Clone, Debug)]
1174enum BreakpointEntryKind {
1175 LineBreakpoint(LineBreakpoint),
1176 ExceptionBreakpoint(ExceptionBreakpoint),
1177 DataBreakpoint(DataBreakpoint),
1178}
1179
1180#[derive(Clone, Debug)]
1181struct BreakpointEntry {
1182 kind: BreakpointEntryKind,
1183 weak: WeakEntity<BreakpointList>,
1184}
1185
1186impl BreakpointEntry {
1187 fn render(
1188 &mut self,
1189 strip_mode: Option<ActiveBreakpointStripMode>,
1190 props: SupportedBreakpointProperties,
1191 ix: usize,
1192 is_selected: bool,
1193 focus_handle: FocusHandle,
1194 ) -> ListItem {
1195 match &mut self.kind {
1196 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
1197 props,
1198 strip_mode,
1199 ix,
1200 is_selected,
1201 focus_handle,
1202 self.weak.clone(),
1203 ),
1204 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
1205 .render(
1206 props.for_exception_breakpoints(),
1207 strip_mode,
1208 ix,
1209 is_selected,
1210 focus_handle,
1211 self.weak.clone(),
1212 ),
1213 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
1214 props.for_data_breakpoints(),
1215 strip_mode,
1216 ix,
1217 is_selected,
1218 focus_handle,
1219 self.weak.clone(),
1220 ),
1221 }
1222 }
1223
1224 fn id(&self) -> SharedString {
1225 match &self.kind {
1226 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
1227 "source-breakpoint-control-strip-{:?}:{}",
1228 line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
1229 )
1230 .into(),
1231 BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
1232 "exception-breakpoint-control-strip--{}",
1233 exception_breakpoint.id
1234 )
1235 .into(),
1236 BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
1237 "data-breakpoint-control-strip--{}",
1238 data_breakpoint.0.dap.data_id
1239 )
1240 .into(),
1241 }
1242 }
1243
1244 fn has_log(&self) -> bool {
1245 match &self.kind {
1246 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1247 line_breakpoint.breakpoint.message.is_some()
1248 }
1249 _ => false,
1250 }
1251 }
1252
1253 fn has_condition(&self) -> bool {
1254 match &self.kind {
1255 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1256 line_breakpoint.breakpoint.condition.is_some()
1257 }
1258 // We don't support conditions on exception/data breakpoints
1259 _ => false,
1260 }
1261 }
1262
1263 fn has_hit_condition(&self) -> bool {
1264 match &self.kind {
1265 BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
1266 line_breakpoint.breakpoint.hit_condition.is_some()
1267 }
1268 _ => false,
1269 }
1270 }
1271}
1272
1273bitflags::bitflags! {
1274 #[derive(Clone, Copy)]
1275 pub struct SupportedBreakpointProperties: u32 {
1276 const LOG = 1 << 0;
1277 const CONDITION = 1 << 1;
1278 const HIT_CONDITION = 1 << 2;
1279 // Conditions for exceptions can be set only when exception filters are supported.
1280 const EXCEPTION_FILTER_OPTIONS = 1 << 3;
1281 }
1282}
1283
1284impl From<&Capabilities> for SupportedBreakpointProperties {
1285 fn from(caps: &Capabilities) -> Self {
1286 let mut this = Self::empty();
1287 for (prop, offset) in [
1288 (caps.supports_log_points, Self::LOG),
1289 (caps.supports_conditional_breakpoints, Self::CONDITION),
1290 (
1291 caps.supports_hit_conditional_breakpoints,
1292 Self::HIT_CONDITION,
1293 ),
1294 (
1295 caps.supports_exception_options,
1296 Self::EXCEPTION_FILTER_OPTIONS,
1297 ),
1298 ] {
1299 if prop.unwrap_or_default() {
1300 this.insert(offset);
1301 }
1302 }
1303 this
1304 }
1305}
1306
1307impl SupportedBreakpointProperties {
1308 fn for_exception_breakpoints(self) -> Self {
1309 // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
1310 Self::empty()
1311 }
1312 fn for_data_breakpoints(self) -> Self {
1313 // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
1314 Self::empty()
1315 }
1316}
1317#[derive(IntoElement)]
1318struct BreakpointOptionsStrip {
1319 props: SupportedBreakpointProperties,
1320 breakpoint: BreakpointEntry,
1321 is_selected: bool,
1322 focus_handle: FocusHandle,
1323 strip_mode: Option<ActiveBreakpointStripMode>,
1324 index: usize,
1325}
1326
1327impl BreakpointOptionsStrip {
1328 fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
1329 self.is_selected && self.strip_mode == Some(expected_mode)
1330 }
1331
1332 fn on_click_callback(
1333 &self,
1334 mode: ActiveBreakpointStripMode,
1335 ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
1336 let list = self.breakpoint.weak.clone();
1337 let ix = self.index;
1338 move |_, window, cx| {
1339 list.update(cx, |this, cx| {
1340 if this.strip_mode != Some(mode) {
1341 this.set_active_breakpoint_property(mode, window, cx);
1342 } else if this.selected_ix == Some(ix) {
1343 this.strip_mode.take();
1344 } else {
1345 cx.propagate();
1346 }
1347 })
1348 .ok();
1349 }
1350 }
1351
1352 fn add_focus_styles(
1353 &self,
1354 kind: ActiveBreakpointStripMode,
1355 available: bool,
1356 window: &Window,
1357 cx: &App,
1358 ) -> impl Fn(Div) -> Div {
1359 move |this: Div| {
1360 // Avoid layout shifts in case there's no colored border
1361 let this = this.border_1().rounded_sm();
1362 let color = cx.theme().colors();
1363
1364 if self.is_selected && self.strip_mode == Some(kind) {
1365 if self.focus_handle.is_focused(window) {
1366 this.bg(color.editor_background)
1367 .border_color(color.border_focused)
1368 } else {
1369 this.border_color(color.border)
1370 }
1371 } else if !available {
1372 this.border_color(color.border_transparent)
1373 } else {
1374 this
1375 }
1376 }
1377 }
1378}
1379
1380impl RenderOnce for BreakpointOptionsStrip {
1381 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1382 let id = self.breakpoint.id();
1383 let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
1384 let supports_condition = self
1385 .props
1386 .contains(SupportedBreakpointProperties::CONDITION);
1387 let supports_hit_condition = self
1388 .props
1389 .contains(SupportedBreakpointProperties::HIT_CONDITION);
1390 let has_logs = self.breakpoint.has_log();
1391 let has_condition = self.breakpoint.has_condition();
1392 let has_hit_condition = self.breakpoint.has_hit_condition();
1393 let style_for_toggle = |mode, is_enabled| {
1394 if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
1395 ui::ButtonStyle::Filled
1396 } else {
1397 ui::ButtonStyle::Subtle
1398 }
1399 };
1400 let color_for_toggle = |is_enabled| {
1401 if is_enabled {
1402 Color::Default
1403 } else {
1404 Color::Muted
1405 }
1406 };
1407
1408 h_flex()
1409 .gap_px()
1410 .mr_3() // Space to avoid overlapping with the scrollbar
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}