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}