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        DropdownMenu::new(
201            "debug-config-menu",
202            last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
203            ContextMenu::build(window, cx, move |mut menu, _, cx| {
204                let setter_for_name = |task: DebugScenario| {
205                    let weak = weak.clone();
206                    move |window: &mut Window, cx: &mut App| {
207                        weak.update(cx, |this, cx| {
208                            this.last_selected_profile_name = Some(SharedString::from(&task.label));
209                            this.debugger = Some(task.adapter.clone());
210                            this.initialize_args = task.initialize_args.clone();
211                            match &task.request {
212                                Some(DebugRequest::Launch(launch_config)) => {
213                                    this.mode = NewSessionMode::launch(
214                                        Some(launch_config.clone()),
215                                        window,
216                                        cx,
217                                    );
218                                }
219                                Some(DebugRequest::Attach(_)) => {
220                                    let Some(workspace) = this.workspace.upgrade() else {
221                                        return;
222                                    };
223                                    this.mode = NewSessionMode::attach(
224                                        this.debugger.clone(),
225                                        workspace,
226                                        window,
227                                        cx,
228                                    );
229                                    this.mode.focus_handle(cx).focus(window);
230                                    if let Some((debugger, attach)) =
231                                        this.debugger.as_ref().zip(this.mode.as_attach())
232                                    {
233                                        Self::update_attach_picker(&attach, &debugger, window, cx);
234                                    }
235                                }
236                                _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
237                            }
238                            cx.notify();
239                        })
240                        .ok();
241                    }
242                };
243
244                let available_tasks: Vec<DebugScenario> = workspace
245                    .update(cx, |this, cx| {
246                        this.project()
247                            .read(cx)
248                            .task_store()
249                            .read(cx)
250                            .task_inventory()
251                            .iter()
252                            .flat_map(|task_inventory| {
253                                task_inventory.read(cx).list_debug_scenarios(None)
254                            })
255                            .collect()
256                    })
257                    .ok()
258                    .unwrap_or_default();
259
260                for debug_definition in available_tasks {
261                    menu = menu.entry(
262                        debug_definition.label.clone(),
263                        None,
264                        setter_for_name(debug_definition),
265                    );
266                }
267                menu
268            }),
269        )
270    }
271}
272
273#[derive(Clone)]
274struct LaunchMode {
275    program: Entity<Editor>,
276    cwd: Entity<Editor>,
277}
278
279impl LaunchMode {
280    fn new(
281        past_launch_config: Option<LaunchRequest>,
282        window: &mut Window,
283        cx: &mut App,
284    ) -> Entity<Self> {
285        let (past_program, past_cwd) = past_launch_config
286            .map(|config| (Some(config.program), config.cwd))
287            .unwrap_or_else(|| (None, None));
288
289        let program = cx.new(|cx| Editor::single_line(window, cx));
290        program.update(cx, |this, cx| {
291            this.set_placeholder_text("Program path", cx);
292
293            if let Some(past_program) = past_program {
294                this.set_text(past_program, window, cx);
295            };
296        });
297        let cwd = cx.new(|cx| Editor::single_line(window, cx));
298        cwd.update(cx, |this, cx| {
299            this.set_placeholder_text("Working Directory", cx);
300            if let Some(past_cwd) = past_cwd {
301                this.set_text(past_cwd.to_string_lossy(), window, cx);
302            };
303        });
304        cx.new(|_| Self { program, cwd })
305    }
306
307    fn debug_task(&self, cx: &App) -> task::LaunchRequest {
308        let path = self.cwd.read(cx).text(cx);
309        task::LaunchRequest {
310            program: self.program.read(cx).text(cx),
311            cwd: path.is_empty().not().then(|| PathBuf::from(path)),
312            args: Default::default(),
313            env: Default::default(),
314        }
315    }
316}
317
318#[derive(Clone)]
319struct AttachMode {
320    definition: DebugTaskDefinition,
321    attach_picker: Entity<AttachModal>,
322}
323
324impl AttachMode {
325    fn new(
326        debugger: Option<SharedString>,
327        workspace: Entity<Workspace>,
328        window: &mut Window,
329        cx: &mut Context<NewSessionModal>,
330    ) -> Entity<Self> {
331        let definition = DebugTaskDefinition {
332            adapter: debugger.clone().unwrap_or_default(),
333            label: "Attach New Session Setup".into(),
334            request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
335            initialize_args: None,
336            tcp_connection: None,
337            stop_on_entry: Some(false),
338        };
339        let attach_picker = cx.new(|cx| {
340            let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
341            window.focus(&modal.focus_handle(cx));
342
343            modal
344        });
345        cx.new(|_| Self {
346            definition,
347            attach_picker,
348        })
349    }
350    fn debug_task(&self) -> task::AttachRequest {
351        task::AttachRequest { process_id: None }
352    }
353}
354
355static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
356static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
357
358#[derive(Clone)]
359enum NewSessionMode {
360    Launch(Entity<LaunchMode>),
361    Attach(Entity<AttachMode>),
362}
363
364impl NewSessionMode {
365    fn debug_task(&self, cx: &App) -> DebugRequest {
366        match self {
367            NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
368            NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
369        }
370    }
371    fn as_attach(&self) -> Option<&Entity<AttachMode>> {
372        if let NewSessionMode::Attach(entity) = self {
373            Some(entity)
374        } else {
375            None
376        }
377    }
378}
379
380impl Focusable for NewSessionMode {
381    fn focus_handle(&self, cx: &App) -> FocusHandle {
382        match &self {
383            NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
384            NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
385        }
386    }
387}
388
389impl RenderOnce for LaunchMode {
390    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
391        v_flex()
392            .p_2()
393            .w_full()
394            .gap_3()
395            .track_focus(&self.program.focus_handle(cx))
396            .child(
397                div().child(
398                    Label::new("Program")
399                        .size(ui::LabelSize::Small)
400                        .color(Color::Muted),
401                ),
402            )
403            .child(render_editor(&self.program, window, cx))
404            .child(
405                div().child(
406                    Label::new("Working Directory")
407                        .size(ui::LabelSize::Small)
408                        .color(Color::Muted),
409                ),
410            )
411            .child(render_editor(&self.cwd, window, cx))
412    }
413}
414
415impl RenderOnce for AttachMode {
416    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
417        v_flex()
418            .w_full()
419            .track_focus(&self.attach_picker.focus_handle(cx))
420            .child(self.attach_picker.clone())
421    }
422}
423
424impl RenderOnce for NewSessionMode {
425    fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
426        match self {
427            NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
428                this.clone().render(window, cx).into_any_element()
429            }),
430            NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
431                this.clone().render(window, cx).into_any_element()
432            }),
433        }
434    }
435}
436
437impl NewSessionMode {
438    fn attach(
439        debugger: Option<SharedString>,
440        workspace: Entity<Workspace>,
441        window: &mut Window,
442        cx: &mut Context<NewSessionModal>,
443    ) -> Self {
444        Self::Attach(AttachMode::new(debugger, workspace, window, cx))
445    }
446    fn launch(
447        past_launch_config: Option<LaunchRequest>,
448        window: &mut Window,
449        cx: &mut Context<NewSessionModal>,
450    ) -> Self {
451        Self::Launch(LaunchMode::new(past_launch_config, window, cx))
452    }
453}
454fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
455    let settings = ThemeSettings::get_global(cx);
456    let theme = cx.theme();
457
458    let text_style = TextStyle {
459        color: cx.theme().colors().text,
460        font_family: settings.buffer_font.family.clone(),
461        font_features: settings.buffer_font.features.clone(),
462        font_size: settings.buffer_font_size(cx).into(),
463        font_weight: settings.buffer_font.weight,
464        line_height: relative(settings.buffer_line_height.value()),
465        background_color: Some(theme.colors().editor_background),
466        ..Default::default()
467    };
468
469    let element = EditorElement::new(
470        editor,
471        EditorStyle {
472            background: theme.colors().editor_background,
473            local_player: theme.players().local(),
474            text: text_style,
475            ..Default::default()
476        },
477    );
478
479    div()
480        .rounded_md()
481        .p_1()
482        .border_1()
483        .border_color(theme.colors().border_variant)
484        .when(
485            editor.focus_handle(cx).contains_focused(window, cx),
486            |this| this.border_color(theme.colors().border_focused),
487        )
488        .child(element)
489        .bg(theme.colors().editor_background)
490}
491
492impl Render for NewSessionModal {
493    fn render(
494        &mut self,
495        window: &mut ui::Window,
496        cx: &mut ui::Context<Self>,
497    ) -> impl ui::IntoElement {
498        v_flex()
499            .size_full()
500            .w(rems(34.))
501            .elevation_3(cx)
502            .bg(cx.theme().colors().elevated_surface_background)
503            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
504                cx.emit(DismissEvent);
505            }))
506            .child(
507                h_flex()
508                    .w_full()
509                    .justify_around()
510                    .p_2()
511                    .child(
512                        h_flex()
513                            .justify_start()
514                            .w_full()
515                            .child(
516                                ToggleButton::new(
517                                    "debugger-session-ui-launch-button",
518                                    "New Session",
519                                )
520                                .size(ButtonSize::Default)
521                                .style(ui::ButtonStyle::Subtle)
522                                .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
523                                .on_click(cx.listener(|this, _, window, cx| {
524                                    this.mode = NewSessionMode::launch(None, window, cx);
525                                    this.mode.focus_handle(cx).focus(window);
526                                    cx.notify();
527                                }))
528                                .first(),
529                            )
530                            .child(
531                                ToggleButton::new(
532                                    "debugger-session-ui-attach-button",
533                                    "Attach to Process",
534                                )
535                                .size(ButtonSize::Default)
536                                .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
537                                .style(ui::ButtonStyle::Subtle)
538                                .on_click(cx.listener(|this, _, window, cx| {
539                                    let Some(workspace) = this.workspace.upgrade() else {
540                                        return;
541                                    };
542                                    this.mode = NewSessionMode::attach(
543                                        this.debugger.clone(),
544                                        workspace,
545                                        window,
546                                        cx,
547                                    );
548                                    this.mode.focus_handle(cx).focus(window);
549                                    if let Some((debugger, attach)) =
550                                        this.debugger.as_ref().zip(this.mode.as_attach())
551                                    {
552                                        Self::update_attach_picker(&attach, &debugger, window, cx);
553                                    }
554
555                                    cx.notify();
556                                }))
557                                .last(),
558                            ),
559                    )
560                    .justify_between()
561                    .child(self.adapter_drop_down_menu(window, cx))
562                    .border_color(cx.theme().colors().border_variant)
563                    .border_b_1(),
564            )
565            .child(v_flex().child(self.mode.clone().render(window, cx)))
566            .child(
567                h_flex()
568                    .justify_between()
569                    .gap_2()
570                    .p_2()
571                    .border_color(cx.theme().colors().border_variant)
572                    .border_t_1()
573                    .w_full()
574                    .child(self.debug_config_drop_down_menu(window, cx))
575                    .child(
576                        h_flex()
577                            .justify_end()
578                            .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
579                                let weak = cx.weak_entity();
580                                this.child(
581                                    CheckboxWithLabel::new(
582                                        "debugger-stop-on-entry",
583                                        Label::new("Stop on Entry").size(ui::LabelSize::Small),
584                                        self.stop_on_entry,
585                                        move |state, _, cx| {
586                                            weak.update(cx, |this, _| {
587                                                this.stop_on_entry = *state;
588                                            })
589                                            .ok();
590                                        },
591                                    )
592                                    .checkbox_position(ui::IconPosition::End),
593                                )
594                            })
595                            .child(
596                                Button::new("debugger-spawn", "Start")
597                                    .on_click(cx.listener(|this, _, window, cx| {
598                                        this.start_new_session(window, cx);
599                                    }))
600                                    .disabled(self.debugger.is_none()),
601                            ),
602                    ),
603            )
604    }
605}
606
607impl EventEmitter<DismissEvent> for NewSessionModal {}
608impl Focusable for NewSessionModal {
609    fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
610        self.mode.focus_handle(cx)
611    }
612}
613
614impl ModalView for NewSessionModal {}