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