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 this.debugger = None;
136 }
137
138 this.launch_picker.update(cx, |picker, cx| {
139 picker.delegate.task_contexts_loaded(
140 task_contexts,
141 languages,
142 window,
143 cx,
144 );
145 picker.refresh(window, cx);
146 cx.notify();
147 });
148 })
149 }
150 })
151 .detach();
152
153 Self {
154 launch_picker,
155 attach_mode,
156 custom_mode,
157 debugger: None,
158 mode: NewSessionMode::Launch,
159 debug_panel: debug_panel.downgrade(),
160 workspace: workspace_handle,
161 save_scenario_state: None,
162 _subscriptions,
163 }
164 });
165 })?;
166
167 anyhow::Ok(())
168 })
169 .detach();
170 }
171
172 fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
173 let dap_menu = self.adapter_drop_down_menu(window, cx);
174 match self.mode {
175 NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
176 this.clone().render(window, cx).into_any_element()
177 }),
178 NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
179 this.clone().render(dap_menu, window, cx).into_any_element()
180 }),
181 NewSessionMode::Launch => v_flex()
182 .w(rems(34.))
183 .child(self.launch_picker.clone())
184 .into_any_element(),
185 }
186 }
187
188 fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
189 match self.mode {
190 NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
191 NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
192 NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
193 }
194 }
195
196 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
197 let request = match self.mode {
198 NewSessionMode::Custom => Some(DebugRequest::Launch(
199 self.custom_mode.read(cx).debug_request(cx),
200 )),
201 NewSessionMode::Attach => Some(DebugRequest::Attach(
202 self.attach_mode.read(cx).debug_request(),
203 )),
204 _ => None,
205 }?;
206 let label = suggested_label(&request, debugger);
207
208 let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
209 Some(self.custom_mode.read(cx).stop_on_entry.selected())
210 } else {
211 None
212 };
213
214 Some(DebugScenario {
215 adapter: debugger.to_owned().into(),
216 label,
217 request: Some(request),
218 initialize_args: None,
219 tcp_connection: None,
220 stop_on_entry,
221 build: None,
222 })
223 }
224
225 fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
226 let Some(debugger) = self.debugger.as_ref() else {
227 return;
228 };
229
230 if let NewSessionMode::Launch = &self.mode {
231 self.launch_picker.update(cx, |picker, cx| {
232 picker.delegate.confirm(false, window, cx);
233 });
234 return;
235 }
236
237 let Some(config) = self.debug_scenario(debugger, cx) else {
238 log::error!("debug config not found in mode: {}", self.mode);
239 return;
240 };
241
242 let debug_panel = self.debug_panel.clone();
243 let Some(task_contexts) = self.task_contexts(cx) else {
244 return;
245 };
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 Result::<_, anyhow::Error>::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 != &this.definition.adapter {
269 this.definition.adapter = adapter.clone();
270
271 this.attach_picker.update(cx, |this, cx| {
272 this.picker.update(cx, |this, cx| {
273 this.delegate.definition.adapter = adapter.clone();
274 this.focus(window, cx);
275 })
276 });
277 }
278
279 cx.notify();
280 })
281 }
282
283 fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
284 self.launch_picker.read(cx).delegate.task_contexts.as_ref()
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: DebugTaskDefinition,
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 = DebugTaskDefinition {
878 adapter: debugger.unwrap_or(DebugAdapterName("".into())),
879 label: "Attach New Session Setup".into(),
880 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
881 initialize_args: None,
882 tcp_connection: None,
883 stop_on_entry: Some(false),
884 };
885 let attach_picker = cx.new(|cx| {
886 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
887 window.focus(&modal.focus_handle(cx));
888
889 modal
890 });
891
892 cx.new(|_| Self {
893 definition,
894 attach_picker,
895 })
896 }
897 pub(super) fn debug_request(&self) -> task::AttachRequest {
898 task::AttachRequest { process_id: None }
899 }
900}
901
902pub(super) struct DebugScenarioDelegate {
903 task_store: Entity<TaskStore>,
904 candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
905 selected_index: usize,
906 matches: Vec<StringMatch>,
907 prompt: String,
908 debug_panel: WeakEntity<DebugPanel>,
909 task_contexts: Option<TaskContexts>,
910 divider_index: Option<usize>,
911 last_used_candidate_index: Option<usize>,
912}
913
914impl DebugScenarioDelegate {
915 pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
916 Self {
917 task_store,
918 candidates: Vec::default(),
919 selected_index: 0,
920 matches: Vec::new(),
921 prompt: String::new(),
922 debug_panel,
923 task_contexts: None,
924 divider_index: None,
925 last_used_candidate_index: None,
926 }
927 }
928
929 fn get_scenario_kind(
930 languages: &Arc<LanguageRegistry>,
931 dap_registry: &DapRegistry,
932 scenario: DebugScenario,
933 ) -> (Option<TaskSourceKind>, DebugScenario) {
934 let language_names = languages.language_names();
935 let language = dap_registry
936 .adapter_language(&scenario.adapter)
937 .map(|language| TaskSourceKind::Language {
938 name: language.into(),
939 });
940
941 let language = language.or_else(|| {
942 scenario
943 .request
944 .as_ref()
945 .and_then(|request| match request {
946 DebugRequest::Launch(launch) => launch
947 .program
948 .rsplit_once(".")
949 .and_then(|split| languages.language_name_for_extension(split.1))
950 .map(|name| TaskSourceKind::Language { name: name.into() }),
951 _ => None,
952 })
953 .or_else(|| {
954 scenario.label.split_whitespace().find_map(|word| {
955 language_names
956 .iter()
957 .find(|name| name.eq_ignore_ascii_case(word))
958 .map(|name| TaskSourceKind::Language {
959 name: name.to_owned().into(),
960 })
961 })
962 })
963 });
964
965 (language, scenario)
966 }
967
968 pub fn task_contexts_loaded(
969 &mut self,
970 task_contexts: TaskContexts,
971 languages: Arc<LanguageRegistry>,
972 _window: &mut Window,
973 cx: &mut Context<Picker<Self>>,
974 ) {
975 self.task_contexts = Some(task_contexts);
976
977 let (recent, scenarios) = self
978 .task_store
979 .update(cx, |task_store, cx| {
980 task_store.task_inventory().map(|inventory| {
981 inventory.update(cx, |inventory, cx| {
982 inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
983 })
984 })
985 })
986 .unwrap_or_default();
987
988 if !recent.is_empty() {
989 self.last_used_candidate_index = Some(recent.len() - 1);
990 }
991
992 let dap_registry = cx.global::<DapRegistry>();
993
994 self.candidates = recent
995 .into_iter()
996 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
997 .chain(scenarios.into_iter().map(|(kind, scenario)| {
998 let (language, scenario) =
999 Self::get_scenario_kind(&languages, &dap_registry, scenario);
1000 (language.or(Some(kind)), scenario)
1001 }))
1002 .collect();
1003 }
1004}
1005
1006impl PickerDelegate for DebugScenarioDelegate {
1007 type ListItem = ui::ListItem;
1008
1009 fn match_count(&self) -> usize {
1010 self.matches.len()
1011 }
1012
1013 fn selected_index(&self) -> usize {
1014 self.selected_index
1015 }
1016
1017 fn set_selected_index(
1018 &mut self,
1019 ix: usize,
1020 _window: &mut Window,
1021 _cx: &mut Context<picker::Picker<Self>>,
1022 ) {
1023 self.selected_index = ix;
1024 }
1025
1026 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1027 "".into()
1028 }
1029
1030 fn update_matches(
1031 &mut self,
1032 query: String,
1033 window: &mut Window,
1034 cx: &mut Context<picker::Picker<Self>>,
1035 ) -> gpui::Task<()> {
1036 let candidates = self.candidates.clone();
1037
1038 cx.spawn_in(window, async move |picker, cx| {
1039 let candidates: Vec<_> = candidates
1040 .into_iter()
1041 .enumerate()
1042 .map(|(index, (_, candidate))| {
1043 StringMatchCandidate::new(index, candidate.label.as_ref())
1044 })
1045 .collect();
1046
1047 let matches = fuzzy::match_strings(
1048 &candidates,
1049 &query,
1050 true,
1051 1000,
1052 &Default::default(),
1053 cx.background_executor().clone(),
1054 )
1055 .await;
1056
1057 picker
1058 .update(cx, |picker, _| {
1059 let delegate = &mut picker.delegate;
1060
1061 delegate.matches = matches;
1062 delegate.prompt = query;
1063
1064 delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1065 let index = delegate
1066 .matches
1067 .partition_point(|matching_task| matching_task.candidate_id <= index);
1068 Some(index).and_then(|index| (index != 0).then(|| index - 1))
1069 });
1070
1071 if delegate.matches.is_empty() {
1072 delegate.selected_index = 0;
1073 } else {
1074 delegate.selected_index =
1075 delegate.selected_index.min(delegate.matches.len() - 1);
1076 }
1077 })
1078 .log_err();
1079 })
1080 }
1081
1082 fn separators_after_indices(&self) -> Vec<usize> {
1083 if let Some(i) = self.divider_index {
1084 vec![i]
1085 } else {
1086 Vec::new()
1087 }
1088 }
1089
1090 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1091 let debug_scenario = self
1092 .matches
1093 .get(self.selected_index())
1094 .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1095
1096 let Some((_, mut debug_scenario)) = debug_scenario else {
1097 return;
1098 };
1099
1100 let (task_context, worktree_id) = self
1101 .task_contexts
1102 .as_ref()
1103 .and_then(|task_contexts| {
1104 Some((
1105 task_contexts.active_context().cloned()?,
1106 task_contexts.worktree(),
1107 ))
1108 })
1109 .unwrap_or_default();
1110
1111 if let Some(launch_config) =
1112 debug_scenario
1113 .request
1114 .as_mut()
1115 .and_then(|request| match request {
1116 DebugRequest::Launch(launch) => Some(launch),
1117 _ => None,
1118 })
1119 {
1120 let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
1121 launch_config.program = program;
1122 }
1123
1124 self.debug_panel
1125 .update(cx, |panel, cx| {
1126 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1127 })
1128 .ok();
1129
1130 cx.emit(DismissEvent);
1131 }
1132
1133 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1134 cx.emit(DismissEvent);
1135 }
1136
1137 fn render_match(
1138 &self,
1139 ix: usize,
1140 selected: bool,
1141 window: &mut Window,
1142 cx: &mut Context<picker::Picker<Self>>,
1143 ) -> Option<Self::ListItem> {
1144 let hit = &self.matches[ix];
1145
1146 let highlighted_location = HighlightedMatch {
1147 text: hit.string.clone(),
1148 highlight_positions: hit.positions.clone(),
1149 char_count: hit.string.chars().count(),
1150 color: Color::Default,
1151 };
1152 let task_kind = &self.candidates[hit.candidate_id].0;
1153
1154 let icon = match task_kind {
1155 Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
1156 Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1157 Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1158 Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1159 Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1160 .get_icon_for_type(&name.to_lowercase(), cx)
1161 .map(Icon::from_path),
1162 None => Some(Icon::new(IconName::HistoryRerun)),
1163 }
1164 .map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
1165
1166 Some(
1167 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1168 .inset(true)
1169 .start_slot::<Icon>(icon)
1170 .spacing(ListItemSpacing::Sparse)
1171 .toggle_state(selected)
1172 .child(highlighted_location.render(window, cx)),
1173 )
1174 }
1175}
1176
1177fn resolve_paths(program: String, path: String) -> (String, String) {
1178 let program = if let Some(program) = program.strip_prefix('~') {
1179 format!(
1180 "$ZED_WORKTREE_ROOT{}{}",
1181 std::path::MAIN_SEPARATOR,
1182 &program
1183 )
1184 } else if !program.starts_with(std::path::MAIN_SEPARATOR) {
1185 format!(
1186 "$ZED_WORKTREE_ROOT{}{}",
1187 std::path::MAIN_SEPARATOR,
1188 &program
1189 )
1190 } else {
1191 program
1192 };
1193
1194 let path = if path.starts_with('~') && !path.is_empty() {
1195 format!(
1196 "$ZED_WORKTREE_ROOT{}{}",
1197 std::path::MAIN_SEPARATOR,
1198 &path[1..]
1199 )
1200 } else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
1201 format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
1202 } else {
1203 path
1204 };
1205
1206 (program, path)
1207}