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    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 {}