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