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