new_session_modal.rs

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