1use std::{
2 borrow::Cow,
3 ops::Not,
4 path::{Path, PathBuf},
5};
6
7use dap::{
8 DapRegistry, DebugRequest,
9 adapters::{DebugAdapterName, DebugTaskDefinition},
10};
11use editor::{Editor, EditorElement, EditorStyle};
12use fuzzy::{StringMatch, StringMatchCandidate};
13use gpui::{
14 App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
15 Subscription, TextStyle, WeakEntity,
16};
17use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
18use project::{TaskSourceKind, task_store::TaskStore};
19use settings::Settings;
20use task::{DebugScenario, LaunchRequest};
21use tasks_ui::task_contexts;
22use theme::ThemeSettings;
23use ui::{
24 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
25 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
26 IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
27 SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
28 relative, rems, v_flex,
29};
30use util::ResultExt;
31use workspace::{ModalView, Workspace};
32
33use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
34
35#[derive(Clone)]
36pub(super) struct NewSessionModal {
37 workspace: WeakEntity<Workspace>,
38 debug_panel: WeakEntity<DebugPanel>,
39 mode: NewSessionMode,
40 stop_on_entry: ToggleState,
41 initialize_args: Option<serde_json::Value>,
42 debugger: Option<DebugAdapterName>,
43 last_selected_profile_name: Option<SharedString>,
44}
45
46fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
47 match request {
48 DebugRequest::Launch(config) => {
49 let last_path_component = Path::new(&config.program)
50 .file_name()
51 .map(|name| name.to_string_lossy())
52 .unwrap_or_else(|| Cow::Borrowed(&config.program));
53
54 format!("{} ({debugger})", last_path_component).into()
55 }
56 DebugRequest::Attach(config) => format!(
57 "pid: {} ({debugger})",
58 config.process_id.unwrap_or(u32::MAX)
59 )
60 .into(),
61 }
62}
63
64impl NewSessionModal {
65 pub(super) fn new(
66 past_debug_definition: Option<DebugTaskDefinition>,
67 debug_panel: WeakEntity<DebugPanel>,
68 workspace: WeakEntity<Workspace>,
69 task_store: Option<Entity<TaskStore>>,
70 window: &mut Window,
71 cx: &mut Context<Self>,
72 ) -> Self {
73 let debugger = past_debug_definition
74 .as_ref()
75 .map(|def| def.adapter.clone());
76
77 let stop_on_entry = past_debug_definition
78 .as_ref()
79 .and_then(|def| def.stop_on_entry);
80
81 let launch_config = match past_debug_definition.map(|def| def.request) {
82 Some(DebugRequest::Launch(launch_config)) => Some(launch_config),
83 _ => None,
84 };
85
86 if let Some(task_store) = task_store {
87 cx.defer_in(window, |this, window, cx| {
88 this.mode = NewSessionMode::scenario(
89 this.debug_panel.clone(),
90 this.workspace.clone(),
91 task_store,
92 window,
93 cx,
94 );
95 });
96 };
97
98 Self {
99 workspace: workspace.clone(),
100 debugger,
101 debug_panel,
102 mode: NewSessionMode::launch(launch_config, window, cx),
103 stop_on_entry: stop_on_entry
104 .map(Into::into)
105 .unwrap_or(ToggleState::Unselected),
106 last_selected_profile_name: None,
107 initialize_args: None,
108 }
109 }
110
111 fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
112 let request = self.mode.debug_task(cx)?;
113 let label = suggested_label(&request, debugger);
114 Some(DebugScenario {
115 adapter: debugger.to_owned().into(),
116 label,
117 request: Some(request),
118 initialize_args: self.initialize_args.clone(),
119 tcp_connection: None,
120 stop_on_entry: match self.stop_on_entry {
121 ToggleState::Selected => Some(true),
122 _ => None,
123 },
124 build: None,
125 })
126 }
127
128 fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
129 let Some(debugger) = self.debugger.as_ref() else {
130 // todo(debugger): show in UI.
131 log::error!("No debugger selected");
132 return;
133 };
134
135 if let NewSessionMode::Scenario(picker) = &self.mode {
136 picker.update(cx, |picker, cx| {
137 picker.delegate.confirm(false, window, cx);
138 });
139 return;
140 }
141
142 let Some(config) = self.debug_config(cx, debugger) else {
143 log::error!("debug config not found in mode: {}", self.mode);
144 return;
145 };
146
147 let debug_panel = self.debug_panel.clone();
148 let workspace = self.workspace.clone();
149 cx.spawn_in(window, async move |this, cx| {
150 let task_contexts = workspace
151 .update_in(cx, |this, window, cx| task_contexts(this, window, cx))?
152 .await;
153 let task_context = task_contexts
154 .active_item_context
155 .map(|(_, _, context)| context)
156 .or_else(|| {
157 task_contexts
158 .active_worktree_context
159 .map(|(_, context)| context)
160 })
161 .unwrap_or_default();
162 debug_panel.update_in(cx, |debug_panel, window, cx| {
163 debug_panel.start_session(config, task_context, None, window, cx)
164 })?;
165 this.update(cx, |_, cx| {
166 cx.emit(DismissEvent);
167 })
168 .ok();
169 anyhow::Result::<_, anyhow::Error>::Ok(())
170 })
171 .detach_and_log_err(cx);
172 }
173
174 fn update_attach_picker(
175 attach: &Entity<AttachMode>,
176 adapter: &DebugAdapterName,
177 window: &mut Window,
178 cx: &mut App,
179 ) {
180 attach.update(cx, |this, cx| {
181 if adapter != &this.definition.adapter {
182 this.definition.adapter = adapter.clone();
183
184 this.attach_picker.update(cx, |this, cx| {
185 this.picker.update(cx, |this, cx| {
186 this.delegate.definition.adapter = adapter.clone();
187 this.focus(window, cx);
188 })
189 });
190 }
191
192 cx.notify();
193 })
194 }
195 fn adapter_drop_down_menu(
196 &self,
197 window: &mut Window,
198 cx: &mut Context<Self>,
199 ) -> ui::DropdownMenu {
200 let workspace = self.workspace.clone();
201 let weak = cx.weak_entity();
202 let label = self
203 .debugger
204 .as_ref()
205 .map(|d| d.0.clone())
206 .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
207 DropdownMenu::new(
208 "dap-adapter-picker",
209 label,
210 ContextMenu::build(window, cx, move |mut menu, _, cx| {
211 let setter_for_name = |name: DebugAdapterName| {
212 let weak = weak.clone();
213 move |window: &mut Window, cx: &mut App| {
214 weak.update(cx, |this, cx| {
215 this.debugger = Some(name.clone());
216 cx.notify();
217 if let NewSessionMode::Attach(attach) = &this.mode {
218 Self::update_attach_picker(&attach, &name, window, cx);
219 }
220 })
221 .ok();
222 }
223 };
224
225 let available_adapters = workspace
226 .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
227 .ok()
228 .unwrap_or_default();
229
230 for adapter in available_adapters {
231 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
232 }
233 menu
234 }),
235 )
236 }
237
238 fn debug_config_drop_down_menu(
239 &self,
240 window: &mut Window,
241 cx: &mut Context<Self>,
242 ) -> ui::DropdownMenu {
243 let workspace = self.workspace.clone();
244 let weak = cx.weak_entity();
245 let last_profile = self.last_selected_profile_name.clone();
246 let worktree = workspace
247 .update(cx, |this, cx| {
248 this.project().read(cx).visible_worktrees(cx).next()
249 })
250 .unwrap_or_default();
251 DropdownMenu::new(
252 "debug-config-menu",
253 last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
254 ContextMenu::build(window, cx, move |mut menu, _, cx| {
255 let setter_for_name = |task: DebugScenario| {
256 let weak = weak.clone();
257 move |window: &mut Window, cx: &mut App| {
258 weak.update(cx, |this, cx| {
259 this.last_selected_profile_name = Some(SharedString::from(&task.label));
260 this.debugger = Some(DebugAdapterName(task.adapter.clone()));
261 this.initialize_args = task.initialize_args.clone();
262 match &task.request {
263 Some(DebugRequest::Launch(launch_config)) => {
264 this.mode = NewSessionMode::launch(
265 Some(launch_config.clone()),
266 window,
267 cx,
268 );
269 }
270 Some(DebugRequest::Attach(_)) => {
271 let Some(workspace) = this.workspace.upgrade() else {
272 return;
273 };
274 this.mode = NewSessionMode::attach(
275 this.debugger.clone(),
276 workspace,
277 window,
278 cx,
279 );
280 this.mode.focus_handle(cx).focus(window);
281 if let Some((debugger, attach)) =
282 this.debugger.as_ref().zip(this.mode.as_attach())
283 {
284 Self::update_attach_picker(&attach, &debugger, window, cx);
285 }
286 }
287 _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
288 }
289 cx.notify();
290 })
291 .ok();
292 }
293 };
294
295 let available_tasks: Vec<DebugScenario> = workspace
296 .update(cx, |this, cx| {
297 this.project()
298 .read(cx)
299 .task_store()
300 .read(cx)
301 .task_inventory()
302 .iter()
303 .flat_map(|task_inventory| {
304 task_inventory.read(cx).list_debug_scenarios(
305 worktree
306 .as_ref()
307 .map(|worktree| worktree.read(cx).id())
308 .iter()
309 .copied(),
310 )
311 })
312 .map(|(_source_kind, scenario)| scenario)
313 .collect()
314 })
315 .ok()
316 .unwrap_or_default();
317
318 for debug_definition in available_tasks {
319 menu = menu.entry(
320 debug_definition.label.clone(),
321 None,
322 setter_for_name(debug_definition),
323 );
324 }
325 menu
326 }),
327 )
328 }
329}
330
331static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
332static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
333
334#[derive(Clone)]
335enum NewSessionMode {
336 Launch(Entity<LaunchMode>),
337 Scenario(Entity<Picker<DebugScenarioDelegate>>),
338 Attach(Entity<AttachMode>),
339}
340
341impl NewSessionMode {
342 fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
343 match self {
344 NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
345 NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
346 NewSessionMode::Scenario(_) => None,
347 }
348 }
349 fn as_attach(&self) -> Option<&Entity<AttachMode>> {
350 if let NewSessionMode::Attach(entity) = self {
351 Some(entity)
352 } else {
353 None
354 }
355 }
356
357 fn scenario(
358 debug_panel: WeakEntity<DebugPanel>,
359 workspace: WeakEntity<Workspace>,
360 task_store: Entity<TaskStore>,
361 window: &mut Window,
362 cx: &mut Context<NewSessionModal>,
363 ) -> NewSessionMode {
364 let picker = cx.new(|cx| {
365 Picker::uniform_list(
366 DebugScenarioDelegate::new(debug_panel, workspace, task_store),
367 window,
368 cx,
369 )
370 .modal(false)
371 });
372
373 cx.subscribe(&picker, |_, _, _, cx| {
374 cx.emit(DismissEvent);
375 })
376 .detach();
377
378 picker.focus_handle(cx).focus(window);
379 NewSessionMode::Scenario(picker)
380 }
381
382 fn attach(
383 debugger: Option<DebugAdapterName>,
384 workspace: Entity<Workspace>,
385 window: &mut Window,
386 cx: &mut Context<NewSessionModal>,
387 ) -> Self {
388 Self::Attach(AttachMode::new(debugger, workspace, window, cx))
389 }
390
391 fn launch(
392 past_launch_config: Option<LaunchRequest>,
393 window: &mut Window,
394 cx: &mut Context<NewSessionModal>,
395 ) -> Self {
396 Self::Launch(LaunchMode::new(past_launch_config, window, cx))
397 }
398
399 fn has_match(&self, cx: &App) -> bool {
400 match self {
401 NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
402 NewSessionMode::Attach(picker) => {
403 picker
404 .read(cx)
405 .attach_picker
406 .read(cx)
407 .picker
408 .read(cx)
409 .delegate
410 .match_count()
411 > 0
412 }
413 _ => false,
414 }
415 }
416}
417
418impl std::fmt::Display for NewSessionMode {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 let mode = match self {
421 NewSessionMode::Launch(_) => "launch".to_owned(),
422 NewSessionMode::Attach(_) => "attach".to_owned(),
423 NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
424 };
425
426 write!(f, "{}", mode)
427 }
428}
429
430impl Focusable for NewSessionMode {
431 fn focus_handle(&self, cx: &App) -> FocusHandle {
432 match &self {
433 NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
434 NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
435 NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
436 }
437 }
438}
439
440impl RenderOnce for NewSessionMode {
441 fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
442 match self {
443 NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
444 this.clone().render(window, cx).into_any_element()
445 }),
446 NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
447 this.clone().render(window, cx).into_any_element()
448 }),
449 NewSessionMode::Scenario(entity) => v_flex()
450 .w(rems(34.))
451 .child(entity.clone())
452 .into_any_element(),
453 }
454 }
455}
456
457fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
458 let settings = ThemeSettings::get_global(cx);
459 let theme = cx.theme();
460
461 let text_style = TextStyle {
462 color: cx.theme().colors().text,
463 font_family: settings.buffer_font.family.clone(),
464 font_features: settings.buffer_font.features.clone(),
465 font_size: settings.buffer_font_size(cx).into(),
466 font_weight: settings.buffer_font.weight,
467 line_height: relative(settings.buffer_line_height.value()),
468 background_color: Some(theme.colors().editor_background),
469 ..Default::default()
470 };
471
472 let element = EditorElement::new(
473 editor,
474 EditorStyle {
475 background: theme.colors().editor_background,
476 local_player: theme.players().local(),
477 text: text_style,
478 ..Default::default()
479 },
480 );
481
482 div()
483 .rounded_md()
484 .p_1()
485 .border_1()
486 .border_color(theme.colors().border_variant)
487 .when(
488 editor.focus_handle(cx).contains_focused(window, cx),
489 |this| this.border_color(theme.colors().border_focused),
490 )
491 .child(element)
492 .bg(theme.colors().editor_background)
493}
494
495impl Render for NewSessionModal {
496 fn render(
497 &mut self,
498 window: &mut ui::Window,
499 cx: &mut ui::Context<Self>,
500 ) -> impl ui::IntoElement {
501 v_flex()
502 .size_full()
503 .w(rems(34.))
504 .elevation_3(cx)
505 .bg(cx.theme().colors().elevated_surface_background)
506 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
507 cx.emit(DismissEvent);
508 }))
509 .child(
510 h_flex()
511 .w_full()
512 .justify_around()
513 .p_2()
514 .child(
515 h_flex()
516 .justify_start()
517 .w_full()
518 .child(
519 ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
520 .size(ButtonSize::Default)
521 .style(ui::ButtonStyle::Subtle)
522 .toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
523 .on_click(cx.listener(|this, _, window, cx| {
524 let Some(task_store) = this
525 .workspace
526 .update(cx, |workspace, cx| {
527 workspace.project().read(cx).task_store().clone()
528 })
529 .ok()
530 else {
531 return;
532 };
533
534 this.mode = NewSessionMode::scenario(
535 this.debug_panel.clone(),
536 this.workspace.clone(),
537 task_store,
538 window,
539 cx,
540 );
541
542 cx.notify();
543 }))
544 .first(),
545 )
546 .child(
547 ToggleButton::new(
548 "debugger-session-ui-launch-button",
549 "New Session",
550 )
551 .size(ButtonSize::Default)
552 .style(ui::ButtonStyle::Subtle)
553 .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
554 .on_click(cx.listener(|this, _, window, cx| {
555 this.mode = NewSessionMode::launch(None, window, cx);
556 this.mode.focus_handle(cx).focus(window);
557 cx.notify();
558 }))
559 .middle(),
560 )
561 .child(
562 ToggleButton::new(
563 "debugger-session-ui-attach-button",
564 "Attach to Process",
565 )
566 .size(ButtonSize::Default)
567 .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
568 .style(ui::ButtonStyle::Subtle)
569 .on_click(cx.listener(|this, _, window, cx| {
570 let Some(workspace) = this.workspace.upgrade() else {
571 return;
572 };
573 this.mode = NewSessionMode::attach(
574 this.debugger.clone(),
575 workspace,
576 window,
577 cx,
578 );
579 this.mode.focus_handle(cx).focus(window);
580 if let Some((debugger, attach)) =
581 this.debugger.as_ref().zip(this.mode.as_attach())
582 {
583 Self::update_attach_picker(&attach, &debugger, window, cx);
584 }
585
586 cx.notify();
587 }))
588 .last(),
589 ),
590 )
591 .justify_between()
592 .child(self.adapter_drop_down_menu(window, cx))
593 .border_color(cx.theme().colors().border_variant)
594 .border_b_1(),
595 )
596 .child(v_flex().child(self.mode.clone().render(window, cx)))
597 .child(
598 h_flex()
599 .justify_between()
600 .gap_2()
601 .p_2()
602 .border_color(cx.theme().colors().border_variant)
603 .border_t_1()
604 .w_full()
605 .child(self.debug_config_drop_down_menu(window, cx))
606 .child(
607 h_flex()
608 .justify_end()
609 .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
610 let weak = cx.weak_entity();
611 this.child(
612 CheckboxWithLabel::new(
613 "debugger-stop-on-entry",
614 Label::new("Stop on Entry").size(ui::LabelSize::Small),
615 self.stop_on_entry,
616 move |state, _, cx| {
617 weak.update(cx, |this, _| {
618 this.stop_on_entry = *state;
619 })
620 .ok();
621 },
622 )
623 .checkbox_position(ui::IconPosition::End),
624 )
625 })
626 .child(
627 Button::new("debugger-spawn", "Start")
628 .on_click(cx.listener(|this, _, window, cx| match &this.mode {
629 NewSessionMode::Scenario(picker) => {
630 picker.update(cx, |picker, cx| {
631 picker.delegate.confirm(true, window, cx)
632 })
633 }
634 _ => this.start_new_session(window, cx),
635 }))
636 .disabled(match self.mode {
637 NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
638 NewSessionMode::Attach(_) => {
639 self.debugger.is_none() || !self.mode.has_match(cx)
640 }
641 NewSessionMode::Launch(_) => self.debugger.is_none(),
642 }),
643 ),
644 ),
645 )
646 }
647}
648
649impl EventEmitter<DismissEvent> for NewSessionModal {}
650impl Focusable for NewSessionModal {
651 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
652 self.mode.focus_handle(cx)
653 }
654}
655
656impl ModalView for NewSessionModal {}
657
658impl RenderOnce for LaunchMode {
659 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
660 v_flex()
661 .p_2()
662 .w_full()
663 .gap_3()
664 .track_focus(&self.program.focus_handle(cx))
665 .child(
666 div().child(
667 Label::new("Program")
668 .size(ui::LabelSize::Small)
669 .color(Color::Muted),
670 ),
671 )
672 .child(render_editor(&self.program, window, cx))
673 .child(
674 div().child(
675 Label::new("Working Directory")
676 .size(ui::LabelSize::Small)
677 .color(Color::Muted),
678 ),
679 )
680 .child(render_editor(&self.cwd, window, cx))
681 }
682}
683
684impl RenderOnce for AttachMode {
685 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
686 v_flex()
687 .w_full()
688 .track_focus(&self.attach_picker.focus_handle(cx))
689 .child(self.attach_picker.clone())
690 }
691}
692
693use std::rc::Rc;
694
695#[derive(Clone)]
696pub(super) struct LaunchMode {
697 program: Entity<Editor>,
698 cwd: Entity<Editor>,
699}
700
701impl LaunchMode {
702 pub(super) fn new(
703 past_launch_config: Option<LaunchRequest>,
704 window: &mut Window,
705 cx: &mut App,
706 ) -> Entity<Self> {
707 let (past_program, past_cwd) = past_launch_config
708 .map(|config| (Some(config.program), config.cwd))
709 .unwrap_or_else(|| (None, None));
710
711 let program = cx.new(|cx| Editor::single_line(window, cx));
712 program.update(cx, |this, cx| {
713 this.set_placeholder_text("Program path", cx);
714
715 if let Some(past_program) = past_program {
716 this.set_text(past_program, window, cx);
717 };
718 });
719 let cwd = cx.new(|cx| Editor::single_line(window, cx));
720 cwd.update(cx, |this, cx| {
721 this.set_placeholder_text("Working Directory", cx);
722 if let Some(past_cwd) = past_cwd {
723 this.set_text(past_cwd.to_string_lossy(), window, cx);
724 };
725 });
726 cx.new(|_| Self { program, cwd })
727 }
728
729 pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
730 let path = self.cwd.read(cx).text(cx);
731 task::LaunchRequest {
732 program: self.program.read(cx).text(cx),
733 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
734 args: Default::default(),
735 env: Default::default(),
736 }
737 }
738}
739
740#[derive(Clone)]
741pub(super) struct AttachMode {
742 pub(super) definition: DebugTaskDefinition,
743 pub(super) attach_picker: Entity<AttachModal>,
744 _subscription: Rc<Subscription>,
745}
746
747impl AttachMode {
748 pub(super) fn new(
749 debugger: Option<DebugAdapterName>,
750 workspace: Entity<Workspace>,
751 window: &mut Window,
752 cx: &mut Context<NewSessionModal>,
753 ) -> Entity<Self> {
754 let definition = DebugTaskDefinition {
755 adapter: debugger.unwrap_or(DebugAdapterName("".into())),
756 label: "Attach New Session Setup".into(),
757 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
758 initialize_args: None,
759 tcp_connection: None,
760 stop_on_entry: Some(false),
761 };
762 let attach_picker = cx.new(|cx| {
763 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
764 window.focus(&modal.focus_handle(cx));
765
766 modal
767 });
768
769 let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
770 cx.emit(DismissEvent);
771 });
772
773 cx.new(|_| Self {
774 definition,
775 attach_picker,
776 _subscription: Rc::new(subscription),
777 })
778 }
779 pub(super) fn debug_task(&self) -> task::AttachRequest {
780 task::AttachRequest { process_id: None }
781 }
782}
783
784pub(super) struct DebugScenarioDelegate {
785 task_store: Entity<TaskStore>,
786 candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
787 selected_index: usize,
788 matches: Vec<StringMatch>,
789 prompt: String,
790 debug_panel: WeakEntity<DebugPanel>,
791 workspace: WeakEntity<Workspace>,
792}
793
794impl DebugScenarioDelegate {
795 pub(super) fn new(
796 debug_panel: WeakEntity<DebugPanel>,
797 workspace: WeakEntity<Workspace>,
798 task_store: Entity<TaskStore>,
799 ) -> Self {
800 Self {
801 task_store,
802 candidates: None,
803 selected_index: 0,
804 matches: Vec::new(),
805 prompt: String::new(),
806 debug_panel,
807 workspace,
808 }
809 }
810}
811
812impl PickerDelegate for DebugScenarioDelegate {
813 type ListItem = ui::ListItem;
814
815 fn match_count(&self) -> usize {
816 self.matches.len()
817 }
818
819 fn selected_index(&self) -> usize {
820 self.selected_index
821 }
822
823 fn set_selected_index(
824 &mut self,
825 ix: usize,
826 _window: &mut Window,
827 _cx: &mut Context<picker::Picker<Self>>,
828 ) {
829 self.selected_index = ix;
830 }
831
832 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
833 "".into()
834 }
835
836 fn update_matches(
837 &mut self,
838 query: String,
839 window: &mut Window,
840 cx: &mut Context<picker::Picker<Self>>,
841 ) -> gpui::Task<()> {
842 let candidates: Vec<_> = match &self.candidates {
843 Some(candidates) => candidates
844 .into_iter()
845 .enumerate()
846 .map(|(index, (_, candidate))| {
847 StringMatchCandidate::new(index, candidate.label.as_ref())
848 })
849 .collect(),
850 None => {
851 let worktree_ids: Vec<_> = self
852 .workspace
853 .update(cx, |this, cx| {
854 this.visible_worktrees(cx)
855 .map(|tree| tree.read(cx).id())
856 .collect()
857 })
858 .ok()
859 .unwrap_or_default();
860
861 let scenarios: Vec<_> = self
862 .task_store
863 .read(cx)
864 .task_inventory()
865 .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
866 .unwrap_or_default();
867
868 self.candidates = Some(scenarios.clone());
869
870 scenarios
871 .into_iter()
872 .enumerate()
873 .map(|(index, (_, candidate))| {
874 StringMatchCandidate::new(index, candidate.label.as_ref())
875 })
876 .collect()
877 }
878 };
879
880 cx.spawn_in(window, async move |picker, cx| {
881 let matches = fuzzy::match_strings(
882 &candidates,
883 &query,
884 true,
885 1000,
886 &Default::default(),
887 cx.background_executor().clone(),
888 )
889 .await;
890
891 picker
892 .update(cx, |picker, _| {
893 let delegate = &mut picker.delegate;
894
895 delegate.matches = matches;
896 delegate.prompt = query;
897
898 if delegate.matches.is_empty() {
899 delegate.selected_index = 0;
900 } else {
901 delegate.selected_index =
902 delegate.selected_index.min(delegate.matches.len() - 1);
903 }
904 })
905 .log_err();
906 })
907 }
908
909 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
910 let debug_scenario = self
911 .matches
912 .get(self.selected_index())
913 .and_then(|match_candidate| {
914 self.candidates
915 .as_ref()
916 .map(|candidates| candidates[match_candidate.candidate_id].clone())
917 });
918
919 let Some((task_source_kind, debug_scenario)) = debug_scenario else {
920 return;
921 };
922
923 let task_context = if let TaskSourceKind::Worktree {
924 id: worktree_id,
925 directory_in_worktree: _,
926 id_base: _,
927 } = task_source_kind
928 {
929 let workspace = self.workspace.clone();
930
931 cx.spawn_in(window, async move |_, cx| {
932 workspace
933 .update_in(cx, |workspace, window, cx| {
934 tasks_ui::task_contexts(workspace, window, cx)
935 })
936 .ok()?
937 .await
938 .task_context_for_worktree_id(worktree_id)
939 .cloned()
940 })
941 } else {
942 gpui::Task::ready(None)
943 };
944
945 cx.spawn_in(window, async move |this, cx| {
946 let task_context = task_context.await.unwrap_or_default();
947
948 this.update_in(cx, |this, window, cx| {
949 this.delegate
950 .debug_panel
951 .update(cx, |panel, cx| {
952 panel.start_session(debug_scenario, task_context, None, window, cx);
953 })
954 .ok();
955
956 cx.emit(DismissEvent);
957 })
958 .ok();
959 })
960 .detach();
961 }
962
963 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
964 cx.emit(DismissEvent);
965 }
966
967 fn render_match(
968 &self,
969 ix: usize,
970 selected: bool,
971 window: &mut Window,
972 cx: &mut Context<picker::Picker<Self>>,
973 ) -> Option<Self::ListItem> {
974 let hit = &self.matches[ix];
975
976 let highlighted_location = HighlightedMatch {
977 text: hit.string.clone(),
978 highlight_positions: hit.positions.clone(),
979 char_count: hit.string.chars().count(),
980 color: Color::Default,
981 };
982
983 let icon = Icon::new(IconName::FileTree)
984 .color(Color::Muted)
985 .size(ui::IconSize::Small);
986
987 Some(
988 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
989 .inset(true)
990 .start_slot::<Icon>(icon)
991 .spacing(ListItemSpacing::Sparse)
992 .toggle_state(selected)
993 .child(highlighted_location.render(window, cx)),
994 )
995 }
996}