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