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