new_session_modal.rs

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