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