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