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}