1use std::{
2 borrow::Cow,
3 ops::Not,
4 path::{Path, PathBuf},
5};
6
7use anyhow::{Result, anyhow};
8use dap::DebugRequestType;
9use editor::{Editor, EditorElement, EditorStyle};
10use gpui::{
11 App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
12 WeakEntity,
13};
14use settings::Settings;
15use task::{DebugTaskDefinition, LaunchConfig};
16use theme::ThemeSettings;
17use ui::{
18 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
19 ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
20 LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
21 ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
22};
23use util::ResultExt;
24use workspace::{ModalView, Workspace};
25
26use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
27
28#[derive(Clone)]
29pub(super) struct NewSessionModal {
30 workspace: WeakEntity<Workspace>,
31 debug_panel: WeakEntity<DebugPanel>,
32 mode: NewSessionMode,
33 stop_on_entry: ToggleState,
34 initialize_args: Option<serde_json::Value>,
35 debugger: Option<SharedString>,
36 last_selected_profile_name: Option<SharedString>,
37}
38
39fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
40 match request {
41 DebugRequestType::Launch(config) => {
42 let last_path_component = Path::new(&config.program)
43 .file_name()
44 .map(|name| name.to_string_lossy())
45 .unwrap_or_else(|| Cow::Borrowed(&config.program));
46
47 format!("{} ({debugger})", last_path_component)
48 }
49 DebugRequestType::Attach(config) => format!(
50 "pid: {} ({debugger})",
51 config.process_id.unwrap_or(u32::MAX)
52 ),
53 }
54}
55
56impl NewSessionModal {
57 pub(super) fn new(
58 past_debug_definition: Option<DebugTaskDefinition>,
59 debug_panel: WeakEntity<DebugPanel>,
60 workspace: WeakEntity<Workspace>,
61 window: &mut Window,
62 cx: &mut App,
63 ) -> Self {
64 let debugger = past_debug_definition
65 .as_ref()
66 .map(|def| def.adapter.clone().into());
67
68 let stop_on_entry = past_debug_definition
69 .as_ref()
70 .and_then(|def| def.stop_on_entry);
71
72 let launch_config = match past_debug_definition.map(|def| def.request) {
73 Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
74 _ => None,
75 };
76
77 Self {
78 workspace: workspace.clone(),
79 debugger,
80 debug_panel,
81 mode: NewSessionMode::launch(launch_config, window, cx),
82 stop_on_entry: stop_on_entry
83 .map(Into::into)
84 .unwrap_or(ToggleState::Unselected),
85 last_selected_profile_name: None,
86 initialize_args: None,
87 }
88 }
89
90 fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
91 let request = self.mode.debug_task(cx);
92 Some(DebugTaskDefinition {
93 adapter: self.debugger.clone()?.to_string(),
94 label: suggested_label(&request, self.debugger.as_deref()?),
95 request,
96 initialize_args: self.initialize_args.clone(),
97 tcp_connection: None,
98 locator: None,
99 stop_on_entry: match self.stop_on_entry {
100 ToggleState::Selected => Some(true),
101 _ => None,
102 },
103 })
104 }
105
106 fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
107 let workspace = self.workspace.clone();
108 let config = self
109 .debug_config(cx)
110 .ok_or_else(|| anyhow!("Failed to create a debug config"))?;
111
112 let _ = self.debug_panel.update(cx, |panel, _| {
113 panel.past_debug_definition = Some(config.clone());
114 });
115
116 let task_contexts = workspace
117 .update(cx, |workspace, cx| {
118 tasks_ui::task_contexts(workspace, window, cx)
119 })
120 .ok();
121
122 cx.spawn(async move |this, cx| {
123 let task_context = if let Some(task) = task_contexts {
124 task.await
125 .active_worktree_context
126 .map_or(task::TaskContext::default(), |context| context.1)
127 } else {
128 task::TaskContext::default()
129 };
130 let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
131
132 let task = project.update(cx, |this, cx| {
133 if let Some(debug_config) =
134 config
135 .clone()
136 .to_zed_format()
137 .ok()
138 .and_then(|task_template| {
139 task_template
140 .resolve_task("debug_task", &task_context)
141 .and_then(|resolved_task| {
142 resolved_task.resolved_debug_adapter_config()
143 })
144 })
145 {
146 this.start_debug_session(debug_config, cx)
147 } else {
148 this.start_debug_session(config.into(), cx)
149 }
150 })?;
151 let spawn_result = task.await;
152 if spawn_result.is_ok() {
153 this.update(cx, |_, cx| {
154 cx.emit(DismissEvent);
155 })
156 .ok();
157 }
158 spawn_result?;
159 anyhow::Result::<_, anyhow::Error>::Ok(())
160 })
161 .detach_and_log_err(cx);
162 Ok(())
163 }
164
165 fn update_attach_picker(
166 attach: &Entity<AttachMode>,
167 selected_debugger: &str,
168 window: &mut Window,
169 cx: &mut App,
170 ) {
171 attach.update(cx, |this, cx| {
172 if selected_debugger != this.debug_definition.adapter {
173 this.debug_definition.adapter = selected_debugger.into();
174 if let Some(project) = this
175 .workspace
176 .read_with(cx, |workspace, _| workspace.project().clone())
177 .ok()
178 {
179 this.attach_picker = Some(cx.new(|cx| {
180 let modal = AttachModal::new(
181 project,
182 this.debug_definition.clone(),
183 false,
184 window,
185 cx,
186 );
187
188 window.focus(&modal.focus_handle(cx));
189
190 modal
191 }));
192 }
193 }
194
195 cx.notify();
196 })
197 }
198 fn adapter_drop_down_menu(
199 &self,
200 window: &mut Window,
201 cx: &mut Context<Self>,
202 ) -> ui::DropdownMenu {
203 let workspace = self.workspace.clone();
204 let weak = cx.weak_entity();
205 let debugger = self.debugger.clone();
206 DropdownMenu::new(
207 "dap-adapter-picker",
208 debugger
209 .as_ref()
210 .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
211 .clone(),
212 ContextMenu::build(window, cx, move |mut menu, _, cx| {
213 let setter_for_name = |name: SharedString| {
214 let weak = weak.clone();
215 move |window: &mut Window, cx: &mut App| {
216 weak.update(cx, |this, cx| {
217 this.debugger = Some(name.clone());
218 cx.notify();
219 if let NewSessionMode::Attach(attach) = &this.mode {
220 Self::update_attach_picker(&attach, &name, window, cx);
221 }
222 })
223 .ok();
224 }
225 };
226
227 let available_adapters = workspace
228 .update(cx, |this, cx| {
229 this.project()
230 .read(cx)
231 .debug_adapters()
232 .enumerate_adapters()
233 })
234 .ok()
235 .unwrap_or_default();
236
237 for adapter in available_adapters {
238 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
239 }
240 menu
241 }),
242 )
243 }
244
245 fn debug_config_drop_down_menu(
246 &self,
247 window: &mut Window,
248 cx: &mut Context<Self>,
249 ) -> ui::DropdownMenu {
250 let workspace = self.workspace.clone();
251 let weak = cx.weak_entity();
252 let last_profile = self.last_selected_profile_name.clone();
253 DropdownMenu::new(
254 "debug-config-menu",
255 last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
256 ContextMenu::build(window, cx, move |mut menu, _, cx| {
257 let setter_for_name = |task: DebugTaskDefinition| {
258 let weak = weak.clone();
259 let workspace = workspace.clone();
260 move |window: &mut Window, cx: &mut App| {
261 weak.update(cx, |this, cx| {
262 this.last_selected_profile_name = Some(SharedString::from(&task.label));
263 this.debugger = Some(task.adapter.clone().into());
264 this.initialize_args = task.initialize_args.clone();
265 match &task.request {
266 DebugRequestType::Launch(launch_config) => {
267 this.mode = NewSessionMode::launch(
268 Some(launch_config.clone()),
269 window,
270 cx,
271 );
272 }
273 DebugRequestType::Attach(_) => {
274 this.mode = NewSessionMode::attach(
275 this.debugger.clone(),
276 workspace.clone(),
277 window,
278 cx,
279 );
280 if let Some((debugger, attach)) =
281 this.debugger.as_ref().zip(this.mode.as_attach())
282 {
283 Self::update_attach_picker(&attach, &debugger, window, cx);
284 }
285 }
286 }
287 cx.notify();
288 })
289 .ok();
290 }
291 };
292
293 let available_adapters: Vec<DebugTaskDefinition> = workspace
294 .update(cx, |this, cx| {
295 this.project()
296 .read(cx)
297 .task_store()
298 .read(cx)
299 .task_inventory()
300 .iter()
301 .flat_map(|task_inventory| task_inventory.read(cx).list_debug_tasks())
302 .cloned()
303 .filter_map(|task| task.try_into().ok())
304 .collect()
305 })
306 .ok()
307 .unwrap_or_default();
308
309 for debug_definition in available_adapters {
310 menu = menu.entry(
311 debug_definition.label.clone(),
312 None,
313 setter_for_name(debug_definition),
314 );
315 }
316 menu
317 }),
318 )
319 }
320}
321
322#[derive(Clone)]
323struct LaunchMode {
324 program: Entity<Editor>,
325 cwd: Entity<Editor>,
326}
327
328impl LaunchMode {
329 fn new(
330 past_launch_config: Option<LaunchConfig>,
331 window: &mut Window,
332 cx: &mut App,
333 ) -> Entity<Self> {
334 let (past_program, past_cwd) = past_launch_config
335 .map(|config| (Some(config.program), config.cwd))
336 .unwrap_or_else(|| (None, None));
337
338 let program = cx.new(|cx| Editor::single_line(window, cx));
339 program.update(cx, |this, cx| {
340 this.set_placeholder_text("Program path", cx);
341
342 if let Some(past_program) = past_program {
343 this.set_text(past_program, window, cx);
344 };
345 });
346 let cwd = cx.new(|cx| Editor::single_line(window, cx));
347 cwd.update(cx, |this, cx| {
348 this.set_placeholder_text("Working Directory", cx);
349 if let Some(past_cwd) = past_cwd {
350 this.set_text(past_cwd.to_string_lossy(), window, cx);
351 };
352 });
353 cx.new(|_| Self { program, cwd })
354 }
355
356 fn debug_task(&self, cx: &App) -> task::LaunchConfig {
357 let path = self.cwd.read(cx).text(cx);
358 task::LaunchConfig {
359 program: self.program.read(cx).text(cx),
360 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
361 args: Default::default(),
362 }
363 }
364}
365
366#[derive(Clone)]
367struct AttachMode {
368 workspace: WeakEntity<Workspace>,
369 debug_definition: DebugTaskDefinition,
370 attach_picker: Option<Entity<AttachModal>>,
371 focus_handle: FocusHandle,
372}
373
374impl AttachMode {
375 fn new(
376 debugger: Option<SharedString>,
377 workspace: WeakEntity<Workspace>,
378 window: &mut Window,
379 cx: &mut App,
380 ) -> Entity<Self> {
381 let debug_definition = DebugTaskDefinition {
382 label: "Attach New Session Setup".into(),
383 request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
384 tcp_connection: None,
385 adapter: debugger.clone().unwrap_or_default().into(),
386 locator: None,
387 initialize_args: None,
388 stop_on_entry: Some(false),
389 };
390
391 let attach_picker = if let Some(project) = debugger.and(
392 workspace
393 .read_with(cx, |workspace, _| workspace.project().clone())
394 .ok(),
395 ) {
396 Some(cx.new(|cx| {
397 let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
398 window.focus(&modal.focus_handle(cx));
399
400 modal
401 }))
402 } else {
403 None
404 };
405
406 cx.new(|cx| Self {
407 workspace,
408 debug_definition,
409 attach_picker,
410 focus_handle: cx.focus_handle(),
411 })
412 }
413 fn debug_task(&self) -> task::AttachConfig {
414 task::AttachConfig { process_id: None }
415 }
416}
417
418static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
419static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
420
421#[derive(Clone)]
422enum NewSessionMode {
423 Launch(Entity<LaunchMode>),
424 Attach(Entity<AttachMode>),
425}
426
427impl NewSessionMode {
428 fn debug_task(&self, cx: &App) -> DebugRequestType {
429 match self {
430 NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
431 NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
432 }
433 }
434 fn as_attach(&self) -> Option<&Entity<AttachMode>> {
435 if let NewSessionMode::Attach(entity) = self {
436 Some(entity)
437 } else {
438 None
439 }
440 }
441}
442
443impl Focusable for NewSessionMode {
444 fn focus_handle(&self, cx: &App) -> FocusHandle {
445 match &self {
446 NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
447 NewSessionMode::Attach(entity) => entity.read(cx).focus_handle.clone(),
448 }
449 }
450}
451
452impl RenderOnce for LaunchMode {
453 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
454 v_flex()
455 .p_2()
456 .w_full()
457 .gap_3()
458 .track_focus(&self.program.focus_handle(cx))
459 .child(
460 div().child(
461 Label::new("Program")
462 .size(ui::LabelSize::Small)
463 .color(Color::Muted),
464 ),
465 )
466 .child(render_editor(&self.program, window, cx))
467 .child(
468 div().child(
469 Label::new("Working Directory")
470 .size(ui::LabelSize::Small)
471 .color(Color::Muted),
472 ),
473 )
474 .child(render_editor(&self.cwd, window, cx))
475 }
476}
477
478impl RenderOnce for AttachMode {
479 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
480 v_flex().w_full().children(self.attach_picker.clone())
481 }
482}
483
484impl RenderOnce for NewSessionMode {
485 fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
486 match self {
487 NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
488 this.clone().render(window, cx).into_any_element()
489 }),
490 NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
491 this.clone().render(window, cx).into_any_element()
492 }),
493 }
494 }
495}
496
497impl NewSessionMode {
498 fn attach(
499 debugger: Option<SharedString>,
500 workspace: WeakEntity<Workspace>,
501 window: &mut Window,
502 cx: &mut App,
503 ) -> Self {
504 Self::Attach(AttachMode::new(debugger, workspace, window, cx))
505 }
506 fn launch(past_launch_config: Option<LaunchConfig>, window: &mut Window, cx: &mut App) -> Self {
507 Self::Launch(LaunchMode::new(past_launch_config, window, cx))
508 }
509}
510fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
511 let settings = ThemeSettings::get_global(cx);
512 let theme = cx.theme();
513
514 let text_style = TextStyle {
515 color: cx.theme().colors().text,
516 font_family: settings.buffer_font.family.clone(),
517 font_features: settings.buffer_font.features.clone(),
518 font_size: settings.buffer_font_size(cx).into(),
519 font_weight: settings.buffer_font.weight,
520 line_height: relative(settings.buffer_line_height.value()),
521 background_color: Some(theme.colors().editor_background),
522 ..Default::default()
523 };
524
525 let element = EditorElement::new(
526 editor,
527 EditorStyle {
528 background: theme.colors().editor_background,
529 local_player: theme.players().local(),
530 text: text_style,
531 ..Default::default()
532 },
533 );
534
535 div()
536 .rounded_md()
537 .p_1()
538 .border_1()
539 .border_color(theme.colors().border_variant)
540 .when(
541 editor.focus_handle(cx).contains_focused(window, cx),
542 |this| this.border_color(theme.colors().border_focused),
543 )
544 .child(element)
545 .bg(theme.colors().editor_background)
546}
547
548impl Render for NewSessionModal {
549 fn render(
550 &mut self,
551 window: &mut ui::Window,
552 cx: &mut ui::Context<Self>,
553 ) -> impl ui::IntoElement {
554 v_flex()
555 .size_full()
556 .w(rems(34.))
557 .elevation_3(cx)
558 .bg(cx.theme().colors().elevated_surface_background)
559 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
560 cx.emit(DismissEvent);
561 }))
562 .child(
563 h_flex()
564 .w_full()
565 .justify_around()
566 .p_2()
567 .child(
568 h_flex()
569 .justify_start()
570 .w_full()
571 .child(
572 ToggleButton::new(
573 "debugger-session-ui-launch-button",
574 "New Session",
575 )
576 .size(ButtonSize::Default)
577 .style(ui::ButtonStyle::Subtle)
578 .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
579 .on_click(cx.listener(|this, _, window, cx| {
580 this.mode = NewSessionMode::launch(None, window, cx);
581 this.mode.focus_handle(cx).focus(window);
582 cx.notify();
583 }))
584 .first(),
585 )
586 .child(
587 ToggleButton::new(
588 "debugger-session-ui-attach-button",
589 "Attach to Process",
590 )
591 .size(ButtonSize::Default)
592 .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
593 .style(ui::ButtonStyle::Subtle)
594 .on_click(cx.listener(|this, _, window, cx| {
595 this.mode = NewSessionMode::attach(
596 this.debugger.clone(),
597 this.workspace.clone(),
598 window,
599 cx,
600 );
601 if let Some((debugger, attach)) =
602 this.debugger.as_ref().zip(this.mode.as_attach())
603 {
604 Self::update_attach_picker(&attach, &debugger, window, cx);
605 }
606 this.mode.focus_handle(cx).focus(window);
607 cx.notify();
608 }))
609 .last(),
610 ),
611 )
612 .justify_between()
613 .child(self.adapter_drop_down_menu(window, cx))
614 .border_color(cx.theme().colors().border_variant)
615 .border_b_1(),
616 )
617 .child(v_flex().child(self.mode.clone().render(window, cx)))
618 .child(
619 h_flex()
620 .justify_between()
621 .gap_2()
622 .p_2()
623 .border_color(cx.theme().colors().border_variant)
624 .border_t_1()
625 .w_full()
626 .child(self.debug_config_drop_down_menu(window, cx))
627 .child(
628 h_flex()
629 .justify_end()
630 .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
631 let weak = cx.weak_entity();
632 this.child(
633 CheckboxWithLabel::new(
634 "debugger-stop-on-entry",
635 Label::new("Stop on Entry").size(ui::LabelSize::Small),
636 self.stop_on_entry,
637 move |state, _, cx| {
638 weak.update(cx, |this, _| {
639 this.stop_on_entry = *state;
640 })
641 .ok();
642 },
643 )
644 .checkbox_position(ui::IconPosition::End),
645 )
646 })
647 .child(
648 Button::new("debugger-spawn", "Start")
649 .on_click(cx.listener(|this, _, window, cx| {
650 this.start_new_session(window, cx).log_err();
651 }))
652 .disabled(self.debugger.is_none()),
653 ),
654 ),
655 )
656 }
657}
658
659impl EventEmitter<DismissEvent> for NewSessionModal {}
660impl Focusable for NewSessionModal {
661 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
662 self.mode.focus_handle(cx)
663 }
664}
665
666impl ModalView for NewSessionModal {}