new_session_modal.rs

  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}