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