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 DropdownMenu::new(
201 "debug-config-menu",
202 last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
203 ContextMenu::build(window, cx, move |mut menu, _, cx| {
204 let setter_for_name = |task: DebugScenario| {
205 let weak = weak.clone();
206 move |window: &mut Window, cx: &mut App| {
207 weak.update(cx, |this, cx| {
208 this.last_selected_profile_name = Some(SharedString::from(&task.label));
209 this.debugger = Some(task.adapter.clone());
210 this.initialize_args = task.initialize_args.clone();
211 match &task.request {
212 Some(DebugRequest::Launch(launch_config)) => {
213 this.mode = NewSessionMode::launch(
214 Some(launch_config.clone()),
215 window,
216 cx,
217 );
218 }
219 Some(DebugRequest::Attach(_)) => {
220 let Some(workspace) = this.workspace.upgrade() else {
221 return;
222 };
223 this.mode = NewSessionMode::attach(
224 this.debugger.clone(),
225 workspace,
226 window,
227 cx,
228 );
229 this.mode.focus_handle(cx).focus(window);
230 if let Some((debugger, attach)) =
231 this.debugger.as_ref().zip(this.mode.as_attach())
232 {
233 Self::update_attach_picker(&attach, &debugger, window, cx);
234 }
235 }
236 _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
237 }
238 cx.notify();
239 })
240 .ok();
241 }
242 };
243
244 let available_tasks: Vec<DebugScenario> = workspace
245 .update(cx, |this, cx| {
246 this.project()
247 .read(cx)
248 .task_store()
249 .read(cx)
250 .task_inventory()
251 .iter()
252 .flat_map(|task_inventory| {
253 task_inventory.read(cx).list_debug_scenarios(None)
254 })
255 .collect()
256 })
257 .ok()
258 .unwrap_or_default();
259
260 for debug_definition in available_tasks {
261 menu = menu.entry(
262 debug_definition.label.clone(),
263 None,
264 setter_for_name(debug_definition),
265 );
266 }
267 menu
268 }),
269 )
270 }
271}
272
273#[derive(Clone)]
274struct LaunchMode {
275 program: Entity<Editor>,
276 cwd: Entity<Editor>,
277}
278
279impl LaunchMode {
280 fn new(
281 past_launch_config: Option<LaunchRequest>,
282 window: &mut Window,
283 cx: &mut App,
284 ) -> Entity<Self> {
285 let (past_program, past_cwd) = past_launch_config
286 .map(|config| (Some(config.program), config.cwd))
287 .unwrap_or_else(|| (None, None));
288
289 let program = cx.new(|cx| Editor::single_line(window, cx));
290 program.update(cx, |this, cx| {
291 this.set_placeholder_text("Program path", cx);
292
293 if let Some(past_program) = past_program {
294 this.set_text(past_program, window, cx);
295 };
296 });
297 let cwd = cx.new(|cx| Editor::single_line(window, cx));
298 cwd.update(cx, |this, cx| {
299 this.set_placeholder_text("Working Directory", cx);
300 if let Some(past_cwd) = past_cwd {
301 this.set_text(past_cwd.to_string_lossy(), window, cx);
302 };
303 });
304 cx.new(|_| Self { program, cwd })
305 }
306
307 fn debug_task(&self, cx: &App) -> task::LaunchRequest {
308 let path = self.cwd.read(cx).text(cx);
309 task::LaunchRequest {
310 program: self.program.read(cx).text(cx),
311 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
312 args: Default::default(),
313 env: Default::default(),
314 }
315 }
316}
317
318#[derive(Clone)]
319struct AttachMode {
320 definition: DebugTaskDefinition,
321 attach_picker: Entity<AttachModal>,
322}
323
324impl AttachMode {
325 fn new(
326 debugger: Option<SharedString>,
327 workspace: Entity<Workspace>,
328 window: &mut Window,
329 cx: &mut Context<NewSessionModal>,
330 ) -> Entity<Self> {
331 let definition = DebugTaskDefinition {
332 adapter: debugger.clone().unwrap_or_default(),
333 label: "Attach New Session Setup".into(),
334 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
335 initialize_args: None,
336 tcp_connection: None,
337 stop_on_entry: Some(false),
338 };
339 let attach_picker = cx.new(|cx| {
340 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
341 window.focus(&modal.focus_handle(cx));
342
343 modal
344 });
345 cx.new(|_| Self {
346 definition,
347 attach_picker,
348 })
349 }
350 fn debug_task(&self) -> task::AttachRequest {
351 task::AttachRequest { process_id: None }
352 }
353}
354
355static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
356static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
357
358#[derive(Clone)]
359enum NewSessionMode {
360 Launch(Entity<LaunchMode>),
361 Attach(Entity<AttachMode>),
362}
363
364impl NewSessionMode {
365 fn debug_task(&self, cx: &App) -> DebugRequest {
366 match self {
367 NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
368 NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
369 }
370 }
371 fn as_attach(&self) -> Option<&Entity<AttachMode>> {
372 if let NewSessionMode::Attach(entity) = self {
373 Some(entity)
374 } else {
375 None
376 }
377 }
378}
379
380impl Focusable for NewSessionMode {
381 fn focus_handle(&self, cx: &App) -> FocusHandle {
382 match &self {
383 NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
384 NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
385 }
386 }
387}
388
389impl RenderOnce for LaunchMode {
390 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
391 v_flex()
392 .p_2()
393 .w_full()
394 .gap_3()
395 .track_focus(&self.program.focus_handle(cx))
396 .child(
397 div().child(
398 Label::new("Program")
399 .size(ui::LabelSize::Small)
400 .color(Color::Muted),
401 ),
402 )
403 .child(render_editor(&self.program, window, cx))
404 .child(
405 div().child(
406 Label::new("Working Directory")
407 .size(ui::LabelSize::Small)
408 .color(Color::Muted),
409 ),
410 )
411 .child(render_editor(&self.cwd, window, cx))
412 }
413}
414
415impl RenderOnce for AttachMode {
416 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
417 v_flex()
418 .w_full()
419 .track_focus(&self.attach_picker.focus_handle(cx))
420 .child(self.attach_picker.clone())
421 }
422}
423
424impl RenderOnce for NewSessionMode {
425 fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
426 match self {
427 NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
428 this.clone().render(window, cx).into_any_element()
429 }),
430 NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
431 this.clone().render(window, cx).into_any_element()
432 }),
433 }
434 }
435}
436
437impl NewSessionMode {
438 fn attach(
439 debugger: Option<SharedString>,
440 workspace: Entity<Workspace>,
441 window: &mut Window,
442 cx: &mut Context<NewSessionModal>,
443 ) -> Self {
444 Self::Attach(AttachMode::new(debugger, workspace, window, cx))
445 }
446 fn launch(
447 past_launch_config: Option<LaunchRequest>,
448 window: &mut Window,
449 cx: &mut Context<NewSessionModal>,
450 ) -> Self {
451 Self::Launch(LaunchMode::new(past_launch_config, window, cx))
452 }
453}
454fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
455 let settings = ThemeSettings::get_global(cx);
456 let theme = cx.theme();
457
458 let text_style = TextStyle {
459 color: cx.theme().colors().text,
460 font_family: settings.buffer_font.family.clone(),
461 font_features: settings.buffer_font.features.clone(),
462 font_size: settings.buffer_font_size(cx).into(),
463 font_weight: settings.buffer_font.weight,
464 line_height: relative(settings.buffer_line_height.value()),
465 background_color: Some(theme.colors().editor_background),
466 ..Default::default()
467 };
468
469 let element = EditorElement::new(
470 editor,
471 EditorStyle {
472 background: theme.colors().editor_background,
473 local_player: theme.players().local(),
474 text: text_style,
475 ..Default::default()
476 },
477 );
478
479 div()
480 .rounded_md()
481 .p_1()
482 .border_1()
483 .border_color(theme.colors().border_variant)
484 .when(
485 editor.focus_handle(cx).contains_focused(window, cx),
486 |this| this.border_color(theme.colors().border_focused),
487 )
488 .child(element)
489 .bg(theme.colors().editor_background)
490}
491
492impl Render for NewSessionModal {
493 fn render(
494 &mut self,
495 window: &mut ui::Window,
496 cx: &mut ui::Context<Self>,
497 ) -> impl ui::IntoElement {
498 v_flex()
499 .size_full()
500 .w(rems(34.))
501 .elevation_3(cx)
502 .bg(cx.theme().colors().elevated_surface_background)
503 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
504 cx.emit(DismissEvent);
505 }))
506 .child(
507 h_flex()
508 .w_full()
509 .justify_around()
510 .p_2()
511 .child(
512 h_flex()
513 .justify_start()
514 .w_full()
515 .child(
516 ToggleButton::new(
517 "debugger-session-ui-launch-button",
518 "New Session",
519 )
520 .size(ButtonSize::Default)
521 .style(ui::ButtonStyle::Subtle)
522 .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
523 .on_click(cx.listener(|this, _, window, cx| {
524 this.mode = NewSessionMode::launch(None, window, cx);
525 this.mode.focus_handle(cx).focus(window);
526 cx.notify();
527 }))
528 .first(),
529 )
530 .child(
531 ToggleButton::new(
532 "debugger-session-ui-attach-button",
533 "Attach to Process",
534 )
535 .size(ButtonSize::Default)
536 .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
537 .style(ui::ButtonStyle::Subtle)
538 .on_click(cx.listener(|this, _, window, cx| {
539 let Some(workspace) = this.workspace.upgrade() else {
540 return;
541 };
542 this.mode = NewSessionMode::attach(
543 this.debugger.clone(),
544 workspace,
545 window,
546 cx,
547 );
548 this.mode.focus_handle(cx).focus(window);
549 if let Some((debugger, attach)) =
550 this.debugger.as_ref().zip(this.mode.as_attach())
551 {
552 Self::update_attach_picker(&attach, &debugger, window, cx);
553 }
554
555 cx.notify();
556 }))
557 .last(),
558 ),
559 )
560 .justify_between()
561 .child(self.adapter_drop_down_menu(window, cx))
562 .border_color(cx.theme().colors().border_variant)
563 .border_b_1(),
564 )
565 .child(v_flex().child(self.mode.clone().render(window, cx)))
566 .child(
567 h_flex()
568 .justify_between()
569 .gap_2()
570 .p_2()
571 .border_color(cx.theme().colors().border_variant)
572 .border_t_1()
573 .w_full()
574 .child(self.debug_config_drop_down_menu(window, cx))
575 .child(
576 h_flex()
577 .justify_end()
578 .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
579 let weak = cx.weak_entity();
580 this.child(
581 CheckboxWithLabel::new(
582 "debugger-stop-on-entry",
583 Label::new("Stop on Entry").size(ui::LabelSize::Small),
584 self.stop_on_entry,
585 move |state, _, cx| {
586 weak.update(cx, |this, _| {
587 this.stop_on_entry = *state;
588 })
589 .ok();
590 },
591 )
592 .checkbox_position(ui::IconPosition::End),
593 )
594 })
595 .child(
596 Button::new("debugger-spawn", "Start")
597 .on_click(cx.listener(|this, _, window, cx| {
598 this.start_new_session(window, cx);
599 }))
600 .disabled(self.debugger.is_none()),
601 ),
602 ),
603 )
604 }
605}
606
607impl EventEmitter<DismissEvent> for NewSessionModal {}
608impl Focusable for NewSessionModal {
609 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
610 self.mode.focus_handle(cx)
611 }
612}
613
614impl ModalView for NewSessionModal {}