inert.rs

  1use std::path::PathBuf;
  2
  3use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
  4use editor::{Editor, EditorElement, EditorStyle};
  5use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
  6use settings::Settings as _;
  7use task::TCPHost;
  8use theme::ThemeSettings;
  9use ui::{
 10    h_flex, relative, v_flex, ActiveTheme as _, ButtonLike, Clickable, Context, ContextMenu,
 11    Disableable, Disclosure, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
 12    LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
 13    SplitButton, Styled, Window,
 14};
 15use workspace::Workspace;
 16
 17use crate::attach_modal::AttachModal;
 18
 19#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
 20enum SpawnMode {
 21    #[default]
 22    Launch,
 23    Attach,
 24}
 25
 26impl SpawnMode {
 27    fn label(&self) -> &'static str {
 28        match self {
 29            SpawnMode::Launch => "Launch",
 30            SpawnMode::Attach => "Attach",
 31        }
 32    }
 33}
 34
 35pub(crate) struct InertState {
 36    focus_handle: FocusHandle,
 37    selected_debugger: Option<SharedString>,
 38    program_editor: Entity<Editor>,
 39    cwd_editor: Entity<Editor>,
 40    workspace: WeakEntity<Workspace>,
 41    spawn_mode: SpawnMode,
 42    popover_handle: PopoverMenuHandle<ContextMenu>,
 43}
 44
 45impl InertState {
 46    pub(super) fn new(
 47        workspace: WeakEntity<Workspace>,
 48        default_cwd: &str,
 49        window: &mut Window,
 50        cx: &mut Context<Self>,
 51    ) -> Self {
 52        let program_editor = cx.new(|cx| {
 53            let mut editor = Editor::single_line(window, cx);
 54            editor.set_placeholder_text("Program path", cx);
 55            editor
 56        });
 57        let cwd_editor = cx.new(|cx| {
 58            let mut editor = Editor::single_line(window, cx);
 59            editor.insert(default_cwd, window, cx);
 60            editor.set_placeholder_text("Working directory", cx);
 61            editor
 62        });
 63        Self {
 64            workspace,
 65            cwd_editor,
 66            program_editor,
 67            selected_debugger: None,
 68            focus_handle: cx.focus_handle(),
 69            spawn_mode: SpawnMode::default(),
 70            popover_handle: Default::default(),
 71        }
 72    }
 73}
 74impl Focusable for InertState {
 75    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 76        self.focus_handle.clone()
 77    }
 78}
 79
 80pub(crate) enum InertEvent {
 81    Spawned { config: DebugAdapterConfig },
 82}
 83
 84impl EventEmitter<InertEvent> for InertState {}
 85
 86static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
 87
 88impl Render for InertState {
 89    fn render(
 90        &mut self,
 91        window: &mut ui::Window,
 92        cx: &mut ui::Context<'_, Self>,
 93    ) -> impl ui::IntoElement {
 94        let weak = cx.weak_entity();
 95        let disable_buttons = self.selected_debugger.is_none();
 96        let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
 97            .child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
 98            .on_click(cx.listener(|this, _, window, cx| {
 99                if this.spawn_mode == SpawnMode::Launch {
100                    let program = this.program_editor.read(cx).text(cx);
101                    let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
102                    let kind =
103                        kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| {
104                            unimplemented!(
105                                "Automatic selection of a debugger based on users project"
106                            )
107                        }));
108                    cx.emit(InertEvent::Spawned {
109                        config: DebugAdapterConfig {
110                            label: "hard coded".into(),
111                            kind,
112                            request: DebugRequestType::Launch,
113                            program: Some(program),
114                            cwd: Some(cwd),
115                            initialize_args: None,
116                            supports_attach: false,
117                        },
118                    });
119                } else {
120                    this.attach(window, cx)
121                }
122            }))
123            .disabled(disable_buttons);
124        v_flex()
125            .track_focus(&self.focus_handle)
126            .size_full()
127            .gap_1()
128            .p_2()
129            .child(
130                v_flex()
131                    .gap_1()
132                    .child(
133                        h_flex()
134                            .w_full()
135                            .gap_2()
136                            .child(Self::render_editor(&self.program_editor, cx))
137                            .child(
138                                h_flex().child(DropdownMenu::new(
139                                    "dap-adapter-picker",
140                                    self.selected_debugger
141                                        .as_ref()
142                                        .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
143                                        .clone(),
144                                    ContextMenu::build(window, cx, move |this, _, _| {
145                                        let setter_for_name = |name: &'static str| {
146                                            let weak = weak.clone();
147                                            move |_: &mut Window, cx: &mut App| {
148                                                let name = name;
149                                                (&weak)
150                                                    .update(cx, move |this, _| {
151                                                        this.selected_debugger = Some(name.into());
152                                                    })
153                                                    .ok();
154                                            }
155                                        };
156                                        this.entry("GDB", None, setter_for_name("GDB"))
157                                            .entry("Delve", None, setter_for_name("Delve"))
158                                            .entry("LLDB", None, setter_for_name("LLDB"))
159                                            .entry("PHP", None, setter_for_name("PHP"))
160                                            .entry(
161                                                "JavaScript",
162                                                None,
163                                                setter_for_name("JavaScript"),
164                                            )
165                                            .entry("Debugpy", None, setter_for_name("Debugpy"))
166                                    }),
167                                )),
168                            ),
169                    )
170                    .child(
171                        h_flex()
172                            .gap_2()
173                            .child(Self::render_editor(&self.cwd_editor, cx))
174                            .map(|this| {
175                                let entity = cx.weak_entity();
176                                this.child(SplitButton {
177                                    left: spawn_button,
178                                    right: PopoverMenu::new("debugger-select-spawn-mode")
179                                        .trigger(Disclosure::new(
180                                            "debugger-spawn-button-disclosure",
181                                            self.popover_handle.is_deployed(),
182                                        ))
183                                        .menu(move |window, cx| {
184                                            Some(ContextMenu::build(window, cx, {
185                                                let entity = entity.clone();
186                                                move |this, _, _| {
187                                                    this.entry("Launch", None, {
188                                                        let entity = entity.clone();
189                                                        move |_, cx| {
190                                                            let _ =
191                                                                entity.update(cx, |this, cx| {
192                                                                    this.spawn_mode =
193                                                                        SpawnMode::Launch;
194                                                                    cx.notify();
195                                                                });
196                                                        }
197                                                    })
198                                                    .entry("Attach", None, {
199                                                        let entity = entity.clone();
200                                                        move |_, cx| {
201                                                            let _ =
202                                                                entity.update(cx, |this, cx| {
203                                                                    this.spawn_mode =
204                                                                        SpawnMode::Attach;
205                                                                    cx.notify();
206                                                                });
207                                                        }
208                                                    })
209                                                }
210                                            }))
211                                        })
212                                        .with_handle(self.popover_handle.clone())
213                                        .into_any_element(),
214                                })
215                            }),
216                    ),
217            )
218    }
219}
220
221fn kind_for_label(label: &str) -> DebugAdapterKind {
222    match label {
223        "LLDB" => DebugAdapterKind::Lldb,
224        "Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
225        "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
226        "PHP" => DebugAdapterKind::Php(TCPHost::default()),
227        "Delve" => DebugAdapterKind::Go(TCPHost::default()),
228        _ => {
229            unimplemented!()
230        } // Maybe we should set a toast notification here
231    }
232}
233impl InertState {
234    fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
235        let settings = ThemeSettings::get_global(cx);
236        let text_style = TextStyle {
237            color: cx.theme().colors().text,
238            font_family: settings.buffer_font.family.clone(),
239            font_features: settings.buffer_font.features.clone(),
240            font_size: settings.buffer_font_size(cx).into(),
241            font_weight: settings.buffer_font.weight,
242            line_height: relative(settings.buffer_line_height.value()),
243            ..Default::default()
244        };
245
246        EditorElement::new(
247            editor,
248            EditorStyle {
249                background: cx.theme().colors().editor_background,
250                local_player: cx.theme().players().local(),
251                text: text_style,
252                ..Default::default()
253            },
254        )
255    }
256
257    fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
258        let process_id = self.program_editor.read(cx).text(cx).parse::<u32>().ok();
259        let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
260        let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
261            unimplemented!("Automatic selection of a debugger based on users project")
262        }));
263
264        let config = DebugAdapterConfig {
265            label: "hard coded attach".into(),
266            kind,
267            request: DebugRequestType::Attach(task::AttachConfig { process_id }),
268            program: None,
269            cwd: Some(cwd),
270            initialize_args: None,
271            supports_attach: true,
272        };
273
274        if process_id.is_some() {
275            cx.emit(InertEvent::Spawned { config });
276        } else {
277            let _ = self.workspace.update(cx, |workspace, cx| {
278                let project = workspace.project().clone();
279                workspace.toggle_modal(window, cx, |window, cx| {
280                    AttachModal::new(project, config, window, cx)
281                });
282            });
283        }
284    }
285}