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