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