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}