new_session_modal.rs

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