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