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