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