new_session_modal.rs

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