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