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