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 .children(self.adapter_drop_down_menu(window, cx))
633 .border_color(cx.theme().colors().border_variant)
634 .border_b_1(),
635 )
636 .child(v_flex().child(self.mode.clone().render(window, cx)))
637 .child(
638 h_flex()
639 .justify_between()
640 .gap_2()
641 .p_2()
642 .border_color(cx.theme().colors().border_variant)
643 .border_t_1()
644 .w_full()
645 .child(
646 matches!(self.mode, NewSessionMode::Scenario(_))
647 .not()
648 .then(|| {
649 self.debug_config_drop_down_menu(window, cx)
650 .into_any_element()
651 })
652 .unwrap_or_else(|| v_flex().w_full().into_any_element()),
653 )
654 .child(
655 h_flex()
656 .justify_end()
657 .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
658 let weak = cx.weak_entity();
659 this.child(
660 CheckboxWithLabel::new(
661 "debugger-stop-on-entry",
662 Label::new("Stop on Entry").size(ui::LabelSize::Small),
663 self.stop_on_entry,
664 move |state, _, cx| {
665 weak.update(cx, |this, _| {
666 this.stop_on_entry = *state;
667 })
668 .ok();
669 },
670 )
671 .checkbox_position(ui::IconPosition::End),
672 )
673 })
674 .child(
675 Button::new("debugger-spawn", "Start")
676 .on_click(cx.listener(|this, _, window, cx| match &this.mode {
677 NewSessionMode::Scenario(picker) => {
678 picker.update(cx, |picker, cx| {
679 picker.delegate.confirm(true, window, cx)
680 })
681 }
682 _ => this.start_new_session(window, cx),
683 }))
684 .disabled(match self.mode {
685 NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
686 NewSessionMode::Attach(_) => {
687 self.debugger.is_none() || !self.mode.has_match(cx)
688 }
689 NewSessionMode::Launch(_) => self.debugger.is_none(),
690 }),
691 ),
692 ),
693 )
694 }
695}
696
697impl EventEmitter<DismissEvent> for NewSessionModal {}
698impl Focusable for NewSessionModal {
699 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
700 self.mode.focus_handle(cx)
701 }
702}
703
704impl ModalView for NewSessionModal {}
705
706impl RenderOnce for LaunchMode {
707 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
708 v_flex()
709 .p_2()
710 .w_full()
711 .gap_3()
712 .track_focus(&self.program.focus_handle(cx))
713 .child(
714 div().child(
715 Label::new("Program")
716 .size(ui::LabelSize::Small)
717 .color(Color::Muted),
718 ),
719 )
720 .child(render_editor(&self.program, window, cx))
721 .child(
722 div().child(
723 Label::new("Working Directory")
724 .size(ui::LabelSize::Small)
725 .color(Color::Muted),
726 ),
727 )
728 .child(render_editor(&self.cwd, window, cx))
729 }
730}
731
732impl RenderOnce for AttachMode {
733 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
734 v_flex()
735 .w_full()
736 .track_focus(&self.attach_picker.focus_handle(cx))
737 .child(self.attach_picker.clone())
738 }
739}
740
741use std::rc::Rc;
742
743#[derive(Clone)]
744pub(super) struct LaunchMode {
745 program: Entity<Editor>,
746 cwd: Entity<Editor>,
747}
748
749impl LaunchMode {
750 pub(super) fn new(
751 past_launch_config: Option<LaunchRequest>,
752 window: &mut Window,
753 cx: &mut App,
754 ) -> Entity<Self> {
755 let (past_program, past_cwd) = past_launch_config
756 .map(|config| (Some(config.program), config.cwd))
757 .unwrap_or_else(|| (None, None));
758
759 let program = cx.new(|cx| Editor::single_line(window, cx));
760 program.update(cx, |this, cx| {
761 this.set_placeholder_text("Program path", cx);
762
763 if let Some(past_program) = past_program {
764 this.set_text(past_program, window, cx);
765 };
766 });
767 let cwd = cx.new(|cx| Editor::single_line(window, cx));
768 cwd.update(cx, |this, cx| {
769 this.set_placeholder_text("Working Directory", cx);
770 if let Some(past_cwd) = past_cwd {
771 this.set_text(past_cwd.to_string_lossy(), window, cx);
772 };
773 });
774 cx.new(|_| Self { program, cwd })
775 }
776
777 pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
778 let path = self.cwd.read(cx).text(cx);
779 task::LaunchRequest {
780 program: self.program.read(cx).text(cx),
781 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
782 args: Default::default(),
783 env: Default::default(),
784 }
785 }
786}
787
788#[derive(Clone)]
789pub(super) struct AttachMode {
790 pub(super) definition: DebugTaskDefinition,
791 pub(super) attach_picker: Entity<AttachModal>,
792 _subscription: Rc<Subscription>,
793}
794
795impl AttachMode {
796 pub(super) fn new(
797 debugger: Option<DebugAdapterName>,
798 workspace: Entity<Workspace>,
799 window: &mut Window,
800 cx: &mut Context<NewSessionModal>,
801 ) -> Entity<Self> {
802 let definition = DebugTaskDefinition {
803 adapter: debugger.unwrap_or(DebugAdapterName("".into())),
804 label: "Attach New Session Setup".into(),
805 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
806 initialize_args: None,
807 tcp_connection: None,
808 stop_on_entry: Some(false),
809 };
810 let attach_picker = cx.new(|cx| {
811 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
812 window.focus(&modal.focus_handle(cx));
813
814 modal
815 });
816
817 let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
818 cx.emit(DismissEvent);
819 });
820
821 cx.new(|_| Self {
822 definition,
823 attach_picker,
824 _subscription: Rc::new(subscription),
825 })
826 }
827 pub(super) fn debug_task(&self) -> task::AttachRequest {
828 task::AttachRequest { process_id: None }
829 }
830}
831
832pub(super) struct DebugScenarioDelegate {
833 task_store: Entity<TaskStore>,
834 candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
835 selected_index: usize,
836 matches: Vec<StringMatch>,
837 prompt: String,
838 debug_panel: WeakEntity<DebugPanel>,
839 workspace: WeakEntity<Workspace>,
840}
841
842impl DebugScenarioDelegate {
843 pub(super) fn new(
844 debug_panel: WeakEntity<DebugPanel>,
845 workspace: WeakEntity<Workspace>,
846 task_store: Entity<TaskStore>,
847 ) -> Self {
848 Self {
849 task_store,
850 candidates: None,
851 selected_index: 0,
852 matches: Vec::new(),
853 prompt: String::new(),
854 debug_panel,
855 workspace,
856 }
857 }
858}
859
860impl PickerDelegate for DebugScenarioDelegate {
861 type ListItem = ui::ListItem;
862
863 fn match_count(&self) -> usize {
864 self.matches.len()
865 }
866
867 fn selected_index(&self) -> usize {
868 self.selected_index
869 }
870
871 fn set_selected_index(
872 &mut self,
873 ix: usize,
874 _window: &mut Window,
875 _cx: &mut Context<picker::Picker<Self>>,
876 ) {
877 self.selected_index = ix;
878 }
879
880 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
881 "".into()
882 }
883
884 fn update_matches(
885 &mut self,
886 query: String,
887 window: &mut Window,
888 cx: &mut Context<picker::Picker<Self>>,
889 ) -> gpui::Task<()> {
890 let candidates: Vec<_> = match &self.candidates {
891 Some(candidates) => candidates
892 .into_iter()
893 .enumerate()
894 .map(|(index, (_, candidate))| {
895 StringMatchCandidate::new(index, candidate.label.as_ref())
896 })
897 .collect(),
898 None => {
899 let worktree_ids: Vec<_> = self
900 .workspace
901 .update(cx, |this, cx| {
902 this.visible_worktrees(cx)
903 .map(|tree| tree.read(cx).id())
904 .collect()
905 })
906 .ok()
907 .unwrap_or_default();
908
909 let scenarios: Vec<_> = self
910 .task_store
911 .read(cx)
912 .task_inventory()
913 .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
914 .unwrap_or_default();
915
916 self.candidates = Some(scenarios.clone());
917
918 scenarios
919 .into_iter()
920 .enumerate()
921 .map(|(index, (_, candidate))| {
922 StringMatchCandidate::new(index, candidate.label.as_ref())
923 })
924 .collect()
925 }
926 };
927
928 cx.spawn_in(window, async move |picker, cx| {
929 let matches = fuzzy::match_strings(
930 &candidates,
931 &query,
932 true,
933 1000,
934 &Default::default(),
935 cx.background_executor().clone(),
936 )
937 .await;
938
939 picker
940 .update(cx, |picker, _| {
941 let delegate = &mut picker.delegate;
942
943 delegate.matches = matches;
944 delegate.prompt = query;
945
946 if delegate.matches.is_empty() {
947 delegate.selected_index = 0;
948 } else {
949 delegate.selected_index =
950 delegate.selected_index.min(delegate.matches.len() - 1);
951 }
952 })
953 .log_err();
954 })
955 }
956
957 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
958 let debug_scenario = self
959 .matches
960 .get(self.selected_index())
961 .and_then(|match_candidate| {
962 self.candidates
963 .as_ref()
964 .map(|candidates| candidates[match_candidate.candidate_id].clone())
965 });
966
967 let Some((task_source_kind, debug_scenario)) = debug_scenario else {
968 return;
969 };
970
971 let task_context = if let TaskSourceKind::Worktree {
972 id: worktree_id,
973 directory_in_worktree: _,
974 id_base: _,
975 } = task_source_kind
976 {
977 let workspace = self.workspace.clone();
978
979 cx.spawn_in(window, async move |_, cx| {
980 workspace
981 .update_in(cx, |workspace, window, cx| {
982 tasks_ui::task_contexts(workspace, window, cx)
983 })
984 .ok()?
985 .await
986 .task_context_for_worktree_id(worktree_id)
987 .cloned()
988 .map(|context| (context, Some(worktree_id)))
989 })
990 } else {
991 gpui::Task::ready(None)
992 };
993
994 cx.spawn_in(window, async move |this, cx| {
995 let (task_context, worktree_id) = task_context.await.unwrap_or_default();
996
997 this.update_in(cx, |this, window, cx| {
998 this.delegate
999 .debug_panel
1000 .update(cx, |panel, cx| {
1001 panel.start_session(
1002 debug_scenario,
1003 task_context,
1004 None,
1005 worktree_id,
1006 window,
1007 cx,
1008 );
1009 })
1010 .ok();
1011
1012 cx.emit(DismissEvent);
1013 })
1014 .ok();
1015 })
1016 .detach();
1017 }
1018
1019 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1020 cx.emit(DismissEvent);
1021 }
1022
1023 fn render_match(
1024 &self,
1025 ix: usize,
1026 selected: bool,
1027 window: &mut Window,
1028 cx: &mut Context<picker::Picker<Self>>,
1029 ) -> Option<Self::ListItem> {
1030 let hit = &self.matches[ix];
1031
1032 let highlighted_location = HighlightedMatch {
1033 text: hit.string.clone(),
1034 highlight_positions: hit.positions.clone(),
1035 char_count: hit.string.chars().count(),
1036 color: Color::Default,
1037 };
1038
1039 let icon = Icon::new(IconName::FileTree)
1040 .color(Color::Muted)
1041 .size(ui::IconSize::Small);
1042
1043 Some(
1044 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1045 .inset(true)
1046 .start_slot::<Icon>(icon)
1047 .spacing(ListItemSpacing::Sparse)
1048 .toggle_state(selected)
1049 .child(highlighted_location.render(window, cx)),
1050 )
1051 }
1052}