inert.rs

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