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