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                                args: Default::default(),
160                            }),
161                            tcp_connection: Some(TCPHost::default()),
162                            initialize_args: None,
163                            locator: None,
164                            stop_on_entry: None,
165                        },
166                    });
167                } else {
168                    this.attach(window, cx)
169                }
170            }))
171            .disabled(disable_buttons);
172
173        v_flex()
174            .track_focus(&self.focus_handle)
175            .size_full()
176            .gap_1()
177            .p_2()
178            .child(
179                v_flex()
180                    .gap_1()
181                    .child(
182                        h_flex()
183                            .w_full()
184                            .gap_2()
185                            .child(Self::render_editor(&self.program_editor, cx))
186                            .child(
187                                h_flex().child(DropdownMenu::new(
188                                    "dap-adapter-picker",
189                                    self.selected_debugger
190                                        .as_ref()
191                                        .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
192                                        .clone(),
193                                    ContextMenu::build(window, cx, move |mut this, _, cx| {
194                                        let setter_for_name = |name: SharedString| {
195                                            let weak = weak.clone();
196                                            move |_: &mut Window, cx: &mut App| {
197                                                let name = name.clone();
198                                                weak.update(cx, move |this, cx| {
199                                                    this.selected_debugger = Some(name.clone());
200                                                    cx.notify();
201                                                })
202                                                .ok();
203                                            }
204                                        };
205                                        let available_adapters = workspace
206                                            .update(cx, |this, cx| {
207                                                this.project()
208                                                    .read(cx)
209                                                    .debug_adapters()
210                                                    .enumerate_adapters()
211                                            })
212                                            .ok()
213                                            .unwrap_or_default();
214
215                                        for adapter in available_adapters {
216                                            this = this.entry(
217                                                adapter.0.clone(),
218                                                None,
219                                                setter_for_name(adapter.0.clone()),
220                                            );
221                                        }
222                                        this
223                                    }),
224                                )),
225                            ),
226                    )
227                    .child(
228                        h_flex()
229                            .gap_2()
230                            .child(Self::render_editor(&self.cwd_editor, cx))
231                            .map(|this| {
232                                let entity = cx.weak_entity();
233                                this.child(SplitButton {
234                                    left: spawn_button,
235                                    right: PopoverMenu::new("debugger-select-spawn-mode")
236                                        .trigger(
237                                            ButtonLike::new_rounded_right(
238                                                "debugger-spawn-button-mode",
239                                            )
240                                            .layer(ui::ElevationIndex::ModalSurface)
241                                            .size(ui::ButtonSize::None)
242                                            .child(
243                                                div().px_1().child(
244                                                    Icon::new(IconName::ChevronDownSmall)
245                                                        .size(IconSize::XSmall),
246                                                ),
247                                            ),
248                                        )
249                                        .menu(move |window, cx| {
250                                            Some(ContextMenu::build(window, cx, {
251                                                let entity = entity.clone();
252                                                move |this, _, _| {
253                                                    this.entry("Launch", None, {
254                                                        let entity = entity.clone();
255                                                        move |_, cx| {
256                                                            let _ =
257                                                                entity.update(cx, |this, cx| {
258                                                                    this.spawn_mode =
259                                                                        SpawnMode::Launch;
260                                                                    cx.notify();
261                                                                });
262                                                        }
263                                                    })
264                                                    .entry("Attach", None, {
265                                                        let entity = entity.clone();
266                                                        move |_, cx| {
267                                                            let _ =
268                                                                entity.update(cx, |this, cx| {
269                                                                    this.spawn_mode =
270                                                                        SpawnMode::Attach;
271                                                                    cx.notify();
272                                                                });
273                                                        }
274                                                    })
275                                                }
276                                            }))
277                                        })
278                                        .with_handle(self.popover_handle.clone())
279                                        .into_any_element(),
280                                })
281                            }),
282                    ),
283            )
284    }
285}
286
287impl InertState {
288    fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
289        let settings = ThemeSettings::get_global(cx);
290        let text_style = TextStyle {
291            color: cx.theme().colors().text,
292            font_family: settings.buffer_font.family.clone(),
293            font_features: settings.buffer_font.features.clone(),
294            font_size: settings.buffer_font_size(cx).into(),
295            font_weight: settings.buffer_font.weight,
296            line_height: relative(settings.buffer_line_height.value()),
297            ..Default::default()
298        };
299
300        EditorElement::new(
301            editor,
302            EditorStyle {
303                background: cx.theme().colors().editor_background,
304                local_player: cx.theme().players().local(),
305                text: text_style,
306                ..Default::default()
307            },
308        )
309    }
310
311    fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
312        let kind = self
313            .selected_debugger
314            .as_deref()
315            .map(|s| s.to_string())
316            .unwrap_or_else(|| {
317                unimplemented!("Automatic selection of a debugger based on users project")
318            });
319
320        let config = DebugTaskDefinition {
321            label: "hard coded attach".into(),
322            adapter: kind,
323            request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
324            initialize_args: None,
325            locator: None,
326            tcp_connection: Some(TCPHost::default()),
327            stop_on_entry: None,
328        };
329
330        let _ = self.workspace.update(cx, |workspace, cx| {
331            let project = workspace.project().clone();
332            workspace.toggle_modal(window, cx, |window, cx| {
333                AttachModal::new(project, config, window, cx)
334            });
335        });
336    }
337}