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