1use collections::FxHashMap;
2use language::LanguageRegistry;
3use std::{
4 borrow::Cow,
5 ops::Not,
6 path::{Path, PathBuf},
7 sync::Arc,
8 time::Duration,
9 usize,
10};
11
12use dap::{
13 DapRegistry, DebugRequest,
14 adapters::{DebugAdapterName, DebugTaskDefinition},
15};
16use editor::{Editor, EditorElement, EditorStyle};
17use fuzzy::{StringMatch, StringMatchCandidate};
18use gpui::{
19 Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
20 Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
21};
22use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
23use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
24use settings::Settings;
25use task::{DebugScenario, LaunchRequest};
26use theme::ThemeSettings;
27use ui::{
28 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
29 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
30 InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
31 ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
32 Toggleable, Window, div, h_flex, relative, rems, v_flex,
33};
34use util::ResultExt;
35use workspace::{ModalView, Workspace, pane};
36
37use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
38
39enum SaveScenarioState {
40 Saving,
41 Saved(ProjectPath),
42 Failed(SharedString),
43}
44
45pub(super) struct NewSessionModal {
46 workspace: WeakEntity<Workspace>,
47 debug_panel: WeakEntity<DebugPanel>,
48 mode: NewSessionMode,
49 launch_picker: Entity<Picker<DebugScenarioDelegate>>,
50 attach_mode: Entity<AttachMode>,
51 custom_mode: Entity<CustomMode>,
52 debugger: Option<DebugAdapterName>,
53 save_scenario_state: Option<SaveScenarioState>,
54 _subscriptions: [Subscription; 2],
55}
56
57fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
58 match request {
59 DebugRequest::Launch(config) => {
60 let last_path_component = Path::new(&config.program)
61 .file_name()
62 .map(|name| name.to_string_lossy())
63 .unwrap_or_else(|| Cow::Borrowed(&config.program));
64
65 format!("{} ({debugger})", last_path_component).into()
66 }
67 DebugRequest::Attach(config) => format!(
68 "pid: {} ({debugger})",
69 config.process_id.unwrap_or(u32::MAX)
70 )
71 .into(),
72 }
73}
74
75impl NewSessionModal {
76 pub(super) fn show(
77 workspace: &mut Workspace,
78 window: &mut Window,
79 cx: &mut Context<Workspace>,
80 ) {
81 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
82 return;
83 };
84 let task_store = workspace.project().read(cx).task_store().clone();
85 let languages = workspace.app_state().languages.clone();
86
87 cx.spawn_in(window, async move |workspace, cx| {
88 workspace.update_in(cx, |workspace, window, cx| {
89 let workspace_handle = workspace.weak_handle();
90 workspace.toggle_modal(window, cx, |window, cx| {
91 let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
92
93 let launch_picker = cx.new(|cx| {
94 Picker::uniform_list(
95 DebugScenarioDelegate::new(debug_panel.downgrade(), task_store),
96 window,
97 cx,
98 )
99 .modal(false)
100 });
101
102 let _subscriptions = [
103 cx.subscribe(&launch_picker, |_, _, _, cx| {
104 cx.emit(DismissEvent);
105 }),
106 cx.subscribe(
107 &attach_mode.read(cx).attach_picker.clone(),
108 |_, _, _, cx| {
109 cx.emit(DismissEvent);
110 },
111 ),
112 ];
113
114 let custom_mode = CustomMode::new(None, window, cx);
115
116 cx.spawn_in(window, {
117 let workspace_handle = workspace_handle.clone();
118 async move |this, cx| {
119 let task_contexts = workspace_handle
120 .update_in(cx, |workspace, window, cx| {
121 tasks_ui::task_contexts(workspace, window, cx)
122 })?
123 .await;
124
125 this.update_in(cx, |this, window, cx| {
126 if let Some(active_cwd) = task_contexts
127 .active_context()
128 .and_then(|context| context.cwd.clone())
129 {
130 this.custom_mode.update(cx, |custom, cx| {
131 custom.load(active_cwd, window, cx);
132 });
133
134 this.debugger = None;
135 }
136
137 this.launch_picker.update(cx, |picker, cx| {
138 picker.delegate.task_contexts_loaded(
139 task_contexts,
140 languages,
141 window,
142 cx,
143 );
144 picker.refresh(window, cx);
145 cx.notify();
146 });
147 })
148 }
149 })
150 .detach();
151
152 Self {
153 launch_picker,
154 attach_mode,
155 custom_mode,
156 debugger: None,
157 mode: NewSessionMode::Launch,
158 debug_panel: debug_panel.downgrade(),
159 workspace: workspace_handle,
160 save_scenario_state: None,
161 _subscriptions,
162 }
163 });
164 })?;
165
166 anyhow::Ok(())
167 })
168 .detach();
169 }
170
171 fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
172 let dap_menu = self.adapter_drop_down_menu(window, cx);
173 match self.mode {
174 NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
175 this.clone().render(window, cx).into_any_element()
176 }),
177 NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
178 this.clone().render(dap_menu, window, cx).into_any_element()
179 }),
180 NewSessionMode::Launch => v_flex()
181 .w(rems(34.))
182 .child(self.launch_picker.clone())
183 .into_any_element(),
184 }
185 }
186
187 fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
188 match self.mode {
189 NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
190 NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
191 NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
192 }
193 }
194
195 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
196 let request = match self.mode {
197 NewSessionMode::Custom => Some(DebugRequest::Launch(
198 self.custom_mode.read(cx).debug_request(cx),
199 )),
200 NewSessionMode::Attach => Some(DebugRequest::Attach(
201 self.attach_mode.read(cx).debug_request(),
202 )),
203 _ => None,
204 }?;
205 let label = suggested_label(&request, debugger);
206
207 let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
208 Some(self.custom_mode.read(cx).stop_on_entry.selected())
209 } else {
210 None
211 };
212
213 Some(DebugScenario {
214 adapter: debugger.to_owned().into(),
215 label,
216 request: Some(request),
217 initialize_args: None,
218 tcp_connection: None,
219 stop_on_entry,
220 build: None,
221 })
222 }
223
224 fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
225 let Some(debugger) = self.debugger.as_ref() else {
226 return;
227 };
228
229 if let NewSessionMode::Launch = &self.mode {
230 self.launch_picker.update(cx, |picker, cx| {
231 picker.delegate.confirm(false, window, cx);
232 });
233 return;
234 }
235
236 let Some(config) = self.debug_scenario(debugger, cx) else {
237 log::error!("debug config not found in mode: {}", self.mode);
238 return;
239 };
240
241 let debug_panel = self.debug_panel.clone();
242 let Some(task_contexts) = self.task_contexts(cx) else {
243 return;
244 };
245 let task_context = task_contexts.active_context().cloned().unwrap_or_default();
246 let worktree_id = task_contexts.worktree();
247 cx.spawn_in(window, async move |this, cx| {
248 debug_panel.update_in(cx, |debug_panel, window, cx| {
249 debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
250 })?;
251 this.update(cx, |_, cx| {
252 cx.emit(DismissEvent);
253 })
254 .ok();
255 anyhow::Ok(())
256 })
257 .detach_and_log_err(cx);
258 }
259
260 fn update_attach_picker(
261 attach: &Entity<AttachMode>,
262 adapter: &DebugAdapterName,
263 window: &mut Window,
264 cx: &mut App,
265 ) {
266 attach.update(cx, |this, cx| {
267 if adapter != &this.definition.adapter {
268 this.definition.adapter = adapter.clone();
269
270 this.attach_picker.update(cx, |this, cx| {
271 this.picker.update(cx, |this, cx| {
272 this.delegate.definition.adapter = adapter.clone();
273 this.focus(window, cx);
274 })
275 });
276 }
277
278 cx.notify();
279 })
280 }
281
282 fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
283 self.launch_picker.read(cx).delegate.task_contexts.as_ref()
284 }
285
286 fn adapter_drop_down_menu(
287 &mut self,
288 window: &mut Window,
289 cx: &mut Context<Self>,
290 ) -> ui::DropdownMenu {
291 let workspace = self.workspace.clone();
292 let weak = cx.weak_entity();
293 let active_buffer = self.task_contexts(cx).and_then(|tc| {
294 tc.active_item_context
295 .as_ref()
296 .and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
297 });
298
299 let active_buffer_language = active_buffer
300 .and_then(|buffer| buffer.read(cx).language())
301 .cloned();
302
303 let mut available_adapters = workspace
304 .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
305 .unwrap_or_default();
306 if let Some(language) = active_buffer_language {
307 available_adapters.sort_by_key(|adapter| {
308 language
309 .config()
310 .debuggers
311 .get_index_of(adapter.0.as_ref())
312 .unwrap_or(usize::MAX)
313 });
314 }
315
316 if self.debugger.is_none() {
317 self.debugger = available_adapters.first().cloned();
318 }
319
320 let label = self
321 .debugger
322 .as_ref()
323 .map(|d| d.0.clone())
324 .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
325
326 DropdownMenu::new(
327 "dap-adapter-picker",
328 label,
329 ContextMenu::build(window, cx, move |mut menu, _, _| {
330 let setter_for_name = |name: DebugAdapterName| {
331 let weak = weak.clone();
332 move |window: &mut Window, cx: &mut App| {
333 weak.update(cx, |this, cx| {
334 this.debugger = Some(name.clone());
335 cx.notify();
336 if let NewSessionMode::Attach = &this.mode {
337 Self::update_attach_picker(&this.attach_mode, &name, window, cx);
338 }
339 })
340 .ok();
341 }
342 };
343
344 for adapter in available_adapters.into_iter() {
345 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
346 }
347
348 menu
349 }),
350 )
351 }
352}
353
354static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
355
356#[derive(Clone)]
357enum NewSessionMode {
358 Custom,
359 Attach,
360 Launch,
361}
362
363impl std::fmt::Display for NewSessionMode {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 let mode = match self {
366 NewSessionMode::Launch => "Launch".to_owned(),
367 NewSessionMode::Attach => "Attach".to_owned(),
368 NewSessionMode::Custom => "Custom".to_owned(),
369 };
370
371 write!(f, "{}", mode)
372 }
373}
374
375impl Focusable for NewSessionMode {
376 fn focus_handle(&self, cx: &App) -> FocusHandle {
377 cx.focus_handle()
378 }
379}
380
381fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
382 let settings = ThemeSettings::get_global(cx);
383 let theme = cx.theme();
384
385 let text_style = TextStyle {
386 color: cx.theme().colors().text,
387 font_family: settings.buffer_font.family.clone(),
388 font_features: settings.buffer_font.features.clone(),
389 font_size: settings.buffer_font_size(cx).into(),
390 font_weight: settings.buffer_font.weight,
391 line_height: relative(settings.buffer_line_height.value()),
392 background_color: Some(theme.colors().editor_background),
393 ..Default::default()
394 };
395
396 let element = EditorElement::new(
397 editor,
398 EditorStyle {
399 background: theme.colors().editor_background,
400 local_player: theme.players().local(),
401 text: text_style,
402 ..Default::default()
403 },
404 );
405
406 div()
407 .rounded_md()
408 .p_1()
409 .border_1()
410 .border_color(theme.colors().border_variant)
411 .when(
412 editor.focus_handle(cx).contains_focused(window, cx),
413 |this| this.border_color(theme.colors().border_focused),
414 )
415 .child(element)
416 .bg(theme.colors().editor_background)
417}
418
419impl Render for NewSessionModal {
420 fn render(
421 &mut self,
422 window: &mut ui::Window,
423 cx: &mut ui::Context<Self>,
424 ) -> impl ui::IntoElement {
425 let this = cx.weak_entity().clone();
426
427 v_flex()
428 .size_full()
429 .w(rems(34.))
430 .key_context("Pane")
431 .elevation_3(cx)
432 .bg(cx.theme().colors().elevated_surface_background)
433 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
434 cx.emit(DismissEvent);
435 }))
436 .on_action(
437 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
438 this.mode = match this.mode {
439 NewSessionMode::Attach => NewSessionMode::Launch,
440 NewSessionMode::Launch => NewSessionMode::Attach,
441 _ => {
442 return;
443 }
444 };
445
446 this.mode_focus_handle(cx).focus(window);
447 }),
448 )
449 .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
450 this.mode = match this.mode {
451 NewSessionMode::Attach => NewSessionMode::Launch,
452 NewSessionMode::Launch => NewSessionMode::Attach,
453 _ => {
454 return;
455 }
456 };
457
458 this.mode_focus_handle(cx).focus(window);
459 }))
460 .child(
461 h_flex()
462 .w_full()
463 .justify_around()
464 .p_2()
465 .child(
466 h_flex()
467 .justify_start()
468 .w_full()
469 .child(
470 ToggleButton::new("debugger-session-ui-picker-button", "Launch")
471 .size(ButtonSize::Default)
472 .style(ui::ButtonStyle::Subtle)
473 .toggle_state(matches!(self.mode, NewSessionMode::Launch))
474 .on_click(cx.listener(|this, _, window, cx| {
475 this.mode = NewSessionMode::Launch;
476 this.mode_focus_handle(cx).focus(window);
477 cx.notify();
478 }))
479 .first(),
480 )
481 .child(
482 ToggleButton::new("debugger-session-ui-attach-button", "Attach")
483 .size(ButtonSize::Default)
484 .toggle_state(matches!(self.mode, NewSessionMode::Attach))
485 .style(ui::ButtonStyle::Subtle)
486 .on_click(cx.listener(|this, _, window, cx| {
487 this.mode = NewSessionMode::Attach;
488
489 if let Some(debugger) = this.debugger.as_ref() {
490 Self::update_attach_picker(
491 &this.attach_mode,
492 &debugger,
493 window,
494 cx,
495 );
496 }
497 this.mode_focus_handle(cx).focus(window);
498 cx.notify();
499 }))
500 .last(),
501 ),
502 )
503 .justify_between()
504 .border_color(cx.theme().colors().border_variant)
505 .border_b_1(),
506 )
507 .child(v_flex().child(self.render_mode(window, cx)))
508 .child(
509 h_flex()
510 .justify_between()
511 .gap_2()
512 .p_2()
513 .border_color(cx.theme().colors().border_variant)
514 .border_t_1()
515 .w_full()
516 .child(match self.mode {
517 NewSessionMode::Attach => {
518 div().child(self.adapter_drop_down_menu(window, cx))
519 }
520 NewSessionMode::Launch => div().child(
521 Button::new("new-session-modal-custom", "Custom").on_click({
522 let this = cx.weak_entity();
523 move |_, window, cx| {
524 this.update(cx, |this, cx| {
525 this.mode = NewSessionMode::Custom;
526 this.mode_focus_handle(cx).focus(window);
527 })
528 .ok();
529 }
530 }),
531 ),
532 NewSessionMode::Custom => h_flex()
533 .child(
534 Button::new("new-session-modal-back", "Save to .zed/debug.json...")
535 .on_click(cx.listener(|this, _, window, cx| {
536 let Some(save_scenario) = this
537 .debugger
538 .as_ref()
539 .and_then(|debugger| this.debug_scenario(&debugger, cx))
540 .zip(
541 this.task_contexts(cx)
542 .and_then(|tcx| tcx.worktree()),
543 )
544 .and_then(|(scenario, worktree_id)| {
545 this.debug_panel
546 .update(cx, |panel, cx| {
547 panel.save_scenario(
548 &scenario,
549 worktree_id,
550 window,
551 cx,
552 )
553 })
554 .ok()
555 })
556 else {
557 return;
558 };
559
560 this.save_scenario_state = Some(SaveScenarioState::Saving);
561
562 cx.spawn(async move |this, cx| {
563 let res = save_scenario.await;
564
565 this.update(cx, |this, _| match res {
566 Ok(saved_file) => {
567 this.save_scenario_state =
568 Some(SaveScenarioState::Saved(saved_file))
569 }
570 Err(error) => {
571 this.save_scenario_state =
572 Some(SaveScenarioState::Failed(
573 error.to_string().into(),
574 ))
575 }
576 })
577 .ok();
578
579 cx.background_executor()
580 .timer(Duration::from_secs(2))
581 .await;
582 this.update(cx, |this, _| {
583 this.save_scenario_state.take()
584 })
585 .ok();
586 })
587 .detach();
588 }))
589 .disabled(
590 self.debugger.is_none()
591 || self
592 .custom_mode
593 .read(cx)
594 .program
595 .read(cx)
596 .is_empty(cx)
597 || self.save_scenario_state.is_some(),
598 ),
599 )
600 .when_some(self.save_scenario_state.as_ref(), {
601 let this_entity = this.clone();
602
603 move |this, save_state| match save_state {
604 SaveScenarioState::Saved(saved_path) => this.child(
605 IconButton::new(
606 "new-session-modal-go-to-file",
607 IconName::ArrowUpRight,
608 )
609 .icon_size(IconSize::Small)
610 .icon_color(Color::Muted)
611 .on_click({
612 let this_entity = this_entity.clone();
613 let saved_path = saved_path.clone();
614 move |_, window, cx| {
615 window
616 .spawn(cx, {
617 let this_entity = this_entity.clone();
618 let saved_path = saved_path.clone();
619
620 async move |cx| {
621 this_entity
622 .update_in(
623 cx,
624 |this, window, cx| {
625 this.workspace.update(
626 cx,
627 |workspace, cx| {
628 workspace.open_path(
629 saved_path
630 .clone(),
631 None,
632 true,
633 window,
634 cx,
635 )
636 },
637 )
638 },
639 )??
640 .await?;
641
642 this_entity
643 .update(cx, |_, cx| {
644 cx.emit(DismissEvent)
645 })
646 .ok();
647
648 anyhow::Ok(())
649 }
650 })
651 .detach();
652 }
653 }),
654 ),
655 SaveScenarioState::Saving => this.child(
656 Icon::new(IconName::Spinner)
657 .size(IconSize::Small)
658 .color(Color::Muted)
659 .with_animation(
660 "Spinner",
661 Animation::new(Duration::from_secs(3)).repeat(),
662 |icon, delta| {
663 icon.transform(Transformation::rotate(
664 percentage(delta),
665 ))
666 },
667 ),
668 ),
669 SaveScenarioState::Failed(error_msg) => this.child(
670 IconButton::new("Failed Scenario Saved", IconName::X)
671 .icon_size(IconSize::Small)
672 .icon_color(Color::Error)
673 .tooltip(ui::Tooltip::text(error_msg.clone())),
674 ),
675 }
676 }),
677 })
678 .child(
679 Button::new("debugger-spawn", "Start")
680 .on_click(cx.listener(|this, _, window, cx| match &this.mode {
681 NewSessionMode::Launch => {
682 this.launch_picker.update(cx, |picker, cx| {
683 picker.delegate.confirm(true, window, cx)
684 })
685 }
686 _ => this.start_new_session(window, cx),
687 }))
688 .disabled(match self.mode {
689 NewSessionMode::Launch => {
690 !self.launch_picker.read(cx).delegate.matches.is_empty()
691 }
692 NewSessionMode::Attach => {
693 self.debugger.is_none()
694 || self
695 .attach_mode
696 .read(cx)
697 .attach_picker
698 .read(cx)
699 .picker
700 .read(cx)
701 .delegate
702 .match_count()
703 == 0
704 }
705 NewSessionMode::Custom => {
706 self.debugger.is_none()
707 || self.custom_mode.read(cx).program.read(cx).is_empty(cx)
708 }
709 }),
710 ),
711 )
712 }
713}
714
715impl EventEmitter<DismissEvent> for NewSessionModal {}
716impl Focusable for NewSessionModal {
717 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
718 self.mode_focus_handle(cx)
719 }
720}
721
722impl ModalView for NewSessionModal {}
723
724impl RenderOnce for AttachMode {
725 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
726 v_flex()
727 .w_full()
728 .track_focus(&self.attach_picker.focus_handle(cx))
729 .child(self.attach_picker.clone())
730 }
731}
732
733#[derive(Clone)]
734pub(super) struct CustomMode {
735 program: Entity<Editor>,
736 cwd: Entity<Editor>,
737 stop_on_entry: ToggleState,
738}
739
740impl CustomMode {
741 pub(super) fn new(
742 past_launch_config: Option<LaunchRequest>,
743 window: &mut Window,
744 cx: &mut App,
745 ) -> Entity<Self> {
746 let (past_program, past_cwd) = past_launch_config
747 .map(|config| (Some(config.program), config.cwd))
748 .unwrap_or_else(|| (None, None));
749
750 let program = cx.new(|cx| Editor::single_line(window, cx));
751 program.update(cx, |this, cx| {
752 this.set_placeholder_text("Run", cx);
753
754 if let Some(past_program) = past_program {
755 this.set_text(past_program, window, cx);
756 };
757 });
758 let cwd = cx.new(|cx| Editor::single_line(window, cx));
759 cwd.update(cx, |this, cx| {
760 this.set_placeholder_text("Working Directory", cx);
761 if let Some(past_cwd) = past_cwd {
762 this.set_text(past_cwd.to_string_lossy(), window, cx);
763 };
764 });
765 cx.new(|_| Self {
766 program,
767 cwd,
768 stop_on_entry: ToggleState::Unselected,
769 })
770 }
771
772 fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
773 self.cwd.update(cx, |editor, cx| {
774 if editor.is_empty(cx) {
775 editor.set_text(cwd.to_string_lossy(), window, cx);
776 }
777 });
778 }
779
780 pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
781 let path = self.cwd.read(cx).text(cx);
782 if cfg!(windows) {
783 return task::LaunchRequest {
784 program: self.program.read(cx).text(cx),
785 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
786 args: Default::default(),
787 env: Default::default(),
788 };
789 }
790 let command = self.program.read(cx).text(cx);
791 let mut args = shlex::split(&command).into_iter().flatten().peekable();
792 let mut env = FxHashMap::default();
793 while args.peek().is_some_and(|arg| arg.contains('=')) {
794 let arg = args.next().unwrap();
795 let (lhs, rhs) = arg.split_once('=').unwrap();
796 env.insert(lhs.to_string(), rhs.to_string());
797 }
798
799 let program = if let Some(program) = args.next() {
800 program
801 } else {
802 env = FxHashMap::default();
803 command
804 };
805
806 let args = args.collect::<Vec<_>>();
807
808 let (program, path) = resolve_paths(program, path);
809
810 task::LaunchRequest {
811 program,
812 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
813 args,
814 env,
815 }
816 }
817
818 fn render(
819 &mut self,
820 adapter_menu: DropdownMenu,
821 window: &mut Window,
822 cx: &mut ui::Context<Self>,
823 ) -> impl IntoElement {
824 v_flex()
825 .p_2()
826 .w_full()
827 .gap_3()
828 .track_focus(&self.program.focus_handle(cx))
829 .child(
830 h_flex()
831 .child(
832 Label::new("Debugger")
833 .size(ui::LabelSize::Small)
834 .color(Color::Muted),
835 )
836 .gap(ui::DynamicSpacing::Base08.rems(cx))
837 .child(adapter_menu),
838 )
839 .child(render_editor(&self.program, window, cx))
840 .child(render_editor(&self.cwd, window, cx))
841 .child(
842 CheckboxWithLabel::new(
843 "debugger-stop-on-entry",
844 Label::new("Stop on Entry")
845 .size(ui::LabelSize::Small)
846 .color(Color::Muted),
847 self.stop_on_entry,
848 {
849 let this = cx.weak_entity();
850 move |state, _, cx| {
851 this.update(cx, |this, _| {
852 this.stop_on_entry = *state;
853 })
854 .ok();
855 }
856 },
857 )
858 .checkbox_position(ui::IconPosition::End),
859 )
860 }
861}
862
863#[derive(Clone)]
864pub(super) struct AttachMode {
865 pub(super) definition: DebugTaskDefinition,
866 pub(super) attach_picker: Entity<AttachModal>,
867}
868
869impl AttachMode {
870 pub(super) fn new(
871 debugger: Option<DebugAdapterName>,
872 workspace: WeakEntity<Workspace>,
873 window: &mut Window,
874 cx: &mut Context<NewSessionModal>,
875 ) -> Entity<Self> {
876 let definition = DebugTaskDefinition {
877 adapter: debugger.unwrap_or(DebugAdapterName("".into())),
878 label: "Attach New Session Setup".into(),
879 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
880 initialize_args: None,
881 tcp_connection: None,
882 stop_on_entry: Some(false),
883 };
884 let attach_picker = cx.new(|cx| {
885 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
886 window.focus(&modal.focus_handle(cx));
887
888 modal
889 });
890
891 cx.new(|_| Self {
892 definition,
893 attach_picker,
894 })
895 }
896 pub(super) fn debug_request(&self) -> task::AttachRequest {
897 task::AttachRequest { process_id: None }
898 }
899}
900
901pub(super) struct DebugScenarioDelegate {
902 task_store: Entity<TaskStore>,
903 candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
904 selected_index: usize,
905 matches: Vec<StringMatch>,
906 prompt: String,
907 debug_panel: WeakEntity<DebugPanel>,
908 task_contexts: Option<TaskContexts>,
909 divider_index: Option<usize>,
910 last_used_candidate_index: Option<usize>,
911}
912
913impl DebugScenarioDelegate {
914 pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
915 Self {
916 task_store,
917 candidates: Vec::default(),
918 selected_index: 0,
919 matches: Vec::new(),
920 prompt: String::new(),
921 debug_panel,
922 task_contexts: None,
923 divider_index: None,
924 last_used_candidate_index: None,
925 }
926 }
927
928 fn get_scenario_kind(
929 languages: &Arc<LanguageRegistry>,
930 dap_registry: &DapRegistry,
931 scenario: DebugScenario,
932 ) -> (Option<TaskSourceKind>, DebugScenario) {
933 let language_names = languages.language_names();
934 let language = dap_registry
935 .adapter_language(&scenario.adapter)
936 .map(|language| TaskSourceKind::Language {
937 name: language.into(),
938 });
939
940 let language = language.or_else(|| {
941 scenario
942 .request
943 .as_ref()
944 .and_then(|request| match request {
945 DebugRequest::Launch(launch) => launch
946 .program
947 .rsplit_once(".")
948 .and_then(|split| languages.language_name_for_extension(split.1))
949 .map(|name| TaskSourceKind::Language { name: name.into() }),
950 _ => None,
951 })
952 .or_else(|| {
953 scenario.label.split_whitespace().find_map(|word| {
954 language_names
955 .iter()
956 .find(|name| name.eq_ignore_ascii_case(word))
957 .map(|name| TaskSourceKind::Language {
958 name: name.to_owned().into(),
959 })
960 })
961 })
962 });
963
964 (language, scenario)
965 }
966
967 pub fn task_contexts_loaded(
968 &mut self,
969 task_contexts: TaskContexts,
970 languages: Arc<LanguageRegistry>,
971 _window: &mut Window,
972 cx: &mut Context<Picker<Self>>,
973 ) {
974 self.task_contexts = Some(task_contexts);
975
976 let (recent, scenarios) = self
977 .task_store
978 .update(cx, |task_store, cx| {
979 task_store.task_inventory().map(|inventory| {
980 inventory.update(cx, |inventory, cx| {
981 inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
982 })
983 })
984 })
985 .unwrap_or_default();
986
987 if !recent.is_empty() {
988 self.last_used_candidate_index = Some(recent.len() - 1);
989 }
990
991 let dap_registry = cx.global::<DapRegistry>();
992
993 self.candidates = recent
994 .into_iter()
995 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
996 .chain(scenarios.into_iter().map(|(kind, scenario)| {
997 let (language, scenario) =
998 Self::get_scenario_kind(&languages, &dap_registry, scenario);
999 (language.or(Some(kind)), scenario)
1000 }))
1001 .collect();
1002 }
1003}
1004
1005impl PickerDelegate for DebugScenarioDelegate {
1006 type ListItem = ui::ListItem;
1007
1008 fn match_count(&self) -> usize {
1009 self.matches.len()
1010 }
1011
1012 fn selected_index(&self) -> usize {
1013 self.selected_index
1014 }
1015
1016 fn set_selected_index(
1017 &mut self,
1018 ix: usize,
1019 _window: &mut Window,
1020 _cx: &mut Context<picker::Picker<Self>>,
1021 ) {
1022 self.selected_index = ix;
1023 }
1024
1025 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1026 "".into()
1027 }
1028
1029 fn update_matches(
1030 &mut self,
1031 query: String,
1032 window: &mut Window,
1033 cx: &mut Context<picker::Picker<Self>>,
1034 ) -> gpui::Task<()> {
1035 let candidates = self.candidates.clone();
1036
1037 cx.spawn_in(window, async move |picker, cx| {
1038 let candidates: Vec<_> = candidates
1039 .into_iter()
1040 .enumerate()
1041 .map(|(index, (_, candidate))| {
1042 StringMatchCandidate::new(index, candidate.label.as_ref())
1043 })
1044 .collect();
1045
1046 let matches = fuzzy::match_strings(
1047 &candidates,
1048 &query,
1049 true,
1050 1000,
1051 &Default::default(),
1052 cx.background_executor().clone(),
1053 )
1054 .await;
1055
1056 picker
1057 .update(cx, |picker, _| {
1058 let delegate = &mut picker.delegate;
1059
1060 delegate.matches = matches;
1061 delegate.prompt = query;
1062
1063 delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1064 let index = delegate
1065 .matches
1066 .partition_point(|matching_task| matching_task.candidate_id <= index);
1067 Some(index).and_then(|index| (index != 0).then(|| index - 1))
1068 });
1069
1070 if delegate.matches.is_empty() {
1071 delegate.selected_index = 0;
1072 } else {
1073 delegate.selected_index =
1074 delegate.selected_index.min(delegate.matches.len() - 1);
1075 }
1076 })
1077 .log_err();
1078 })
1079 }
1080
1081 fn separators_after_indices(&self) -> Vec<usize> {
1082 if let Some(i) = self.divider_index {
1083 vec![i]
1084 } else {
1085 Vec::new()
1086 }
1087 }
1088
1089 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1090 let debug_scenario = self
1091 .matches
1092 .get(self.selected_index())
1093 .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1094
1095 let Some((_, mut debug_scenario)) = debug_scenario else {
1096 return;
1097 };
1098
1099 let (task_context, worktree_id) = self
1100 .task_contexts
1101 .as_ref()
1102 .and_then(|task_contexts| {
1103 Some((
1104 task_contexts.active_context().cloned()?,
1105 task_contexts.worktree(),
1106 ))
1107 })
1108 .unwrap_or_default();
1109
1110 if let Some(launch_config) =
1111 debug_scenario
1112 .request
1113 .as_mut()
1114 .and_then(|request| match request {
1115 DebugRequest::Launch(launch) => Some(launch),
1116 _ => None,
1117 })
1118 {
1119 let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
1120 launch_config.program = program;
1121 }
1122
1123 self.debug_panel
1124 .update(cx, |panel, cx| {
1125 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1126 })
1127 .ok();
1128
1129 cx.emit(DismissEvent);
1130 }
1131
1132 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1133 cx.emit(DismissEvent);
1134 }
1135
1136 fn render_match(
1137 &self,
1138 ix: usize,
1139 selected: bool,
1140 window: &mut Window,
1141 cx: &mut Context<picker::Picker<Self>>,
1142 ) -> Option<Self::ListItem> {
1143 let hit = &self.matches[ix];
1144
1145 let highlighted_location = HighlightedMatch {
1146 text: hit.string.clone(),
1147 highlight_positions: hit.positions.clone(),
1148 char_count: hit.string.chars().count(),
1149 color: Color::Default,
1150 };
1151 let task_kind = &self.candidates[hit.candidate_id].0;
1152
1153 let icon = match task_kind {
1154 Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
1155 Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1156 Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1157 Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1158 Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1159 .get_icon_for_type(&name.to_lowercase(), cx)
1160 .map(Icon::from_path),
1161 None => Some(Icon::new(IconName::HistoryRerun)),
1162 }
1163 .map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
1164
1165 Some(
1166 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1167 .inset(true)
1168 .start_slot::<Icon>(icon)
1169 .spacing(ListItemSpacing::Sparse)
1170 .toggle_state(selected)
1171 .child(highlighted_location.render(window, cx)),
1172 )
1173 }
1174}
1175
1176fn resolve_paths(program: String, path: String) -> (String, String) {
1177 let program = if let Some(program) = program.strip_prefix('~') {
1178 format!(
1179 "$ZED_WORKTREE_ROOT{}{}",
1180 std::path::MAIN_SEPARATOR,
1181 &program
1182 )
1183 } else if !program.starts_with(std::path::MAIN_SEPARATOR) {
1184 format!(
1185 "$ZED_WORKTREE_ROOT{}{}",
1186 std::path::MAIN_SEPARATOR,
1187 &program
1188 )
1189 } else {
1190 program
1191 };
1192
1193 let path = if path.starts_with('~') && !path.is_empty() {
1194 format!(
1195 "$ZED_WORKTREE_ROOT{}{}",
1196 std::path::MAIN_SEPARATOR,
1197 &path[1..]
1198 )
1199 } else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
1200 format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
1201 } else {
1202 path
1203 };
1204
1205 (program, path)
1206}