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