new_session_modal.rs

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