new_session_modal.rs

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