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("Run", cx);
754
755 if let Some(past_program) = past_program {
756 this.set_text(past_program, window, cx);
757 };
758 });
759 let cwd = cx.new(|cx| Editor::single_line(window, cx));
760 cwd.update(cx, |this, cx| {
761 this.set_placeholder_text("Working Directory", cx);
762 if let Some(past_cwd) = past_cwd {
763 this.set_text(past_cwd.to_string_lossy(), window, cx);
764 };
765 });
766 cx.new(|_| Self {
767 program,
768 cwd,
769 stop_on_entry: ToggleState::Unselected,
770 })
771 }
772
773 fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
774 self.cwd.update(cx, |editor, cx| {
775 if editor.is_empty(cx) {
776 editor.set_text(cwd.to_string_lossy(), window, cx);
777 }
778 });
779 }
780
781 pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
782 let path = self.cwd.read(cx).text(cx);
783 if cfg!(windows) {
784 return task::LaunchRequest {
785 program: self.program.read(cx).text(cx),
786 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
787 args: Default::default(),
788 env: Default::default(),
789 };
790 }
791 let command = self.program.read(cx).text(cx);
792 let mut args = shlex::split(&command).into_iter().flatten().peekable();
793 let mut env = FxHashMap::default();
794 while args.peek().is_some_and(|arg| arg.contains('=')) {
795 let arg = args.next().unwrap();
796 let (lhs, rhs) = arg.split_once('=').unwrap();
797 env.insert(lhs.to_string(), rhs.to_string());
798 }
799
800 let program = if let Some(program) = args.next() {
801 program
802 } else {
803 env = FxHashMap::default();
804 command
805 };
806
807 let args = args.collect::<Vec<_>>();
808
809 let (program, path) = resolve_paths(program, path);
810
811 task::LaunchRequest {
812 program,
813 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
814 args,
815 env,
816 }
817 }
818
819 fn render(
820 &mut self,
821 adapter_menu: DropdownMenu,
822 window: &mut Window,
823 cx: &mut ui::Context<Self>,
824 ) -> impl IntoElement {
825 v_flex()
826 .p_2()
827 .w_full()
828 .gap_3()
829 .track_focus(&self.program.focus_handle(cx))
830 .child(
831 h_flex()
832 .child(
833 Label::new("Debugger")
834 .size(ui::LabelSize::Small)
835 .color(Color::Muted),
836 )
837 .gap(ui::DynamicSpacing::Base08.rems(cx))
838 .child(adapter_menu),
839 )
840 .child(render_editor(&self.program, window, cx))
841 .child(render_editor(&self.cwd, window, cx))
842 .child(
843 CheckboxWithLabel::new(
844 "debugger-stop-on-entry",
845 Label::new("Stop on Entry")
846 .size(ui::LabelSize::Small)
847 .color(Color::Muted),
848 self.stop_on_entry,
849 {
850 let this = cx.weak_entity();
851 move |state, _, cx| {
852 this.update(cx, |this, _| {
853 this.stop_on_entry = *state;
854 })
855 .ok();
856 }
857 },
858 )
859 .checkbox_position(ui::IconPosition::End),
860 )
861 }
862}
863
864#[derive(Clone)]
865pub(super) struct AttachMode {
866 pub(super) definition: ZedDebugConfig,
867 pub(super) attach_picker: Entity<AttachModal>,
868}
869
870impl AttachMode {
871 pub(super) fn new(
872 debugger: Option<DebugAdapterName>,
873 workspace: WeakEntity<Workspace>,
874 window: &mut Window,
875 cx: &mut Context<NewSessionModal>,
876 ) -> Entity<Self> {
877 let definition = ZedDebugConfig {
878 adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
879 label: "Attach New Session Setup".into(),
880 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
881 stop_on_entry: Some(false),
882 };
883 let attach_picker = cx.new(|cx| {
884 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
885 window.focus(&modal.focus_handle(cx));
886
887 modal
888 });
889
890 cx.new(|_| Self {
891 definition,
892 attach_picker,
893 })
894 }
895 pub(super) fn debug_request(&self) -> task::AttachRequest {
896 task::AttachRequest { process_id: None }
897 }
898}
899
900pub(super) struct DebugScenarioDelegate {
901 task_store: Entity<TaskStore>,
902 candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
903 selected_index: usize,
904 matches: Vec<StringMatch>,
905 prompt: String,
906 debug_panel: WeakEntity<DebugPanel>,
907 task_contexts: Option<Arc<TaskContexts>>,
908 divider_index: Option<usize>,
909 last_used_candidate_index: Option<usize>,
910}
911
912impl DebugScenarioDelegate {
913 pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
914 Self {
915 task_store,
916 candidates: Vec::default(),
917 selected_index: 0,
918 matches: Vec::new(),
919 prompt: String::new(),
920 debug_panel,
921 task_contexts: None,
922 divider_index: None,
923 last_used_candidate_index: None,
924 }
925 }
926
927 fn get_scenario_kind(
928 languages: &Arc<LanguageRegistry>,
929 dap_registry: &DapRegistry,
930 scenario: DebugScenario,
931 ) -> (Option<TaskSourceKind>, DebugScenario) {
932 let language_names = languages.language_names();
933 let language = dap_registry
934 .adapter_language(&scenario.adapter)
935 .map(|language| TaskSourceKind::Language {
936 name: language.into(),
937 });
938
939 let language = language.or_else(|| {
940 scenario.label.split_whitespace().find_map(|word| {
941 language_names
942 .iter()
943 .find(|name| name.eq_ignore_ascii_case(word))
944 .map(|name| TaskSourceKind::Language {
945 name: name.to_owned().into(),
946 })
947 })
948 });
949
950 (language, scenario)
951 }
952
953 pub fn task_contexts_loaded(
954 &mut self,
955 task_contexts: TaskContexts,
956 languages: Arc<LanguageRegistry>,
957 _window: &mut Window,
958 cx: &mut Context<Picker<Self>>,
959 ) {
960 self.task_contexts = Some(Arc::new(task_contexts));
961
962 let (recent, scenarios) = self
963 .task_store
964 .update(cx, |task_store, cx| {
965 task_store.task_inventory().map(|inventory| {
966 inventory.update(cx, |inventory, cx| {
967 inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
968 })
969 })
970 })
971 .unwrap_or_default();
972
973 if !recent.is_empty() {
974 self.last_used_candidate_index = Some(recent.len() - 1);
975 }
976
977 let dap_registry = cx.global::<DapRegistry>();
978
979 self.candidates = recent
980 .into_iter()
981 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
982 .chain(scenarios.into_iter().map(|(kind, scenario)| {
983 let (language, scenario) =
984 Self::get_scenario_kind(&languages, &dap_registry, scenario);
985 (language.or(Some(kind)), scenario)
986 }))
987 .collect();
988 }
989}
990
991impl PickerDelegate for DebugScenarioDelegate {
992 type ListItem = ui::ListItem;
993
994 fn match_count(&self) -> usize {
995 self.matches.len()
996 }
997
998 fn selected_index(&self) -> usize {
999 self.selected_index
1000 }
1001
1002 fn set_selected_index(
1003 &mut self,
1004 ix: usize,
1005 _window: &mut Window,
1006 _cx: &mut Context<picker::Picker<Self>>,
1007 ) {
1008 self.selected_index = ix;
1009 }
1010
1011 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1012 "".into()
1013 }
1014
1015 fn update_matches(
1016 &mut self,
1017 query: String,
1018 window: &mut Window,
1019 cx: &mut Context<picker::Picker<Self>>,
1020 ) -> gpui::Task<()> {
1021 let candidates = self.candidates.clone();
1022
1023 cx.spawn_in(window, async move |picker, cx| {
1024 let candidates: Vec<_> = candidates
1025 .into_iter()
1026 .enumerate()
1027 .map(|(index, (_, candidate))| {
1028 StringMatchCandidate::new(index, candidate.label.as_ref())
1029 })
1030 .collect();
1031
1032 let matches = fuzzy::match_strings(
1033 &candidates,
1034 &query,
1035 true,
1036 1000,
1037 &Default::default(),
1038 cx.background_executor().clone(),
1039 )
1040 .await;
1041
1042 picker
1043 .update(cx, |picker, _| {
1044 let delegate = &mut picker.delegate;
1045
1046 delegate.matches = matches;
1047 delegate.prompt = query;
1048
1049 delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1050 let index = delegate
1051 .matches
1052 .partition_point(|matching_task| matching_task.candidate_id <= index);
1053 Some(index).and_then(|index| (index != 0).then(|| index - 1))
1054 });
1055
1056 if delegate.matches.is_empty() {
1057 delegate.selected_index = 0;
1058 } else {
1059 delegate.selected_index =
1060 delegate.selected_index.min(delegate.matches.len() - 1);
1061 }
1062 })
1063 .log_err();
1064 })
1065 }
1066
1067 fn separators_after_indices(&self) -> Vec<usize> {
1068 if let Some(i) = self.divider_index {
1069 vec![i]
1070 } else {
1071 Vec::new()
1072 }
1073 }
1074
1075 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1076 let debug_scenario = self
1077 .matches
1078 .get(self.selected_index())
1079 .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1080
1081 let Some((_, debug_scenario)) = debug_scenario else {
1082 return;
1083 };
1084
1085 let (task_context, worktree_id) = self
1086 .task_contexts
1087 .as_ref()
1088 .and_then(|task_contexts| {
1089 Some((
1090 task_contexts.active_context().cloned()?,
1091 task_contexts.worktree(),
1092 ))
1093 })
1094 .unwrap_or_default();
1095
1096 send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1097 self.debug_panel
1098 .update(cx, |panel, cx| {
1099 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1100 })
1101 .ok();
1102
1103 cx.emit(DismissEvent);
1104 }
1105
1106 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1107 cx.emit(DismissEvent);
1108 }
1109
1110 fn render_match(
1111 &self,
1112 ix: usize,
1113 selected: bool,
1114 window: &mut Window,
1115 cx: &mut Context<picker::Picker<Self>>,
1116 ) -> Option<Self::ListItem> {
1117 let hit = &self.matches[ix];
1118
1119 let highlighted_location = HighlightedMatch {
1120 text: hit.string.clone(),
1121 highlight_positions: hit.positions.clone(),
1122 char_count: hit.string.chars().count(),
1123 color: Color::Default,
1124 };
1125 let task_kind = &self.candidates[hit.candidate_id].0;
1126
1127 let icon = match task_kind {
1128 Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
1129 Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1130 Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1131 Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1132 Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1133 .get_icon_for_type(&name.to_lowercase(), cx)
1134 .map(Icon::from_path),
1135 None => Some(Icon::new(IconName::HistoryRerun)),
1136 }
1137 .map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
1138
1139 Some(
1140 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1141 .inset(true)
1142 .start_slot::<Icon>(icon)
1143 .spacing(ListItemSpacing::Sparse)
1144 .toggle_state(selected)
1145 .child(highlighted_location.render(window, cx)),
1146 )
1147 }
1148}
1149
1150fn resolve_paths(program: String, path: String) -> (String, String) {
1151 let program = if let Some(program) = program.strip_prefix('~') {
1152 format!(
1153 "$ZED_WORKTREE_ROOT{}{}",
1154 std::path::MAIN_SEPARATOR,
1155 &program
1156 )
1157 } else if !program.starts_with(std::path::MAIN_SEPARATOR) {
1158 format!(
1159 "$ZED_WORKTREE_ROOT{}{}",
1160 std::path::MAIN_SEPARATOR,
1161 &program
1162 )
1163 } else {
1164 program
1165 };
1166
1167 let path = if path.starts_with('~') && !path.is_empty() {
1168 format!(
1169 "$ZED_WORKTREE_ROOT{}{}",
1170 std::path::MAIN_SEPARATOR,
1171 &path[1..]
1172 )
1173 } else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
1174 format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
1175 } else {
1176 path
1177 };
1178
1179 (program, path)
1180}