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
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(
180 ButtonLike::new_rounded_right(
181 "debugger-spawn-button-mode",
182 )
183 .layer(ui::ElevationIndex::ModalSurface)
184 .size(ui::ButtonSize::None)
185 .child(
186 div().px_1().child(
187 Icon::new(IconName::ChevronDownSmall)
188 .size(IconSize::XSmall),
189 ),
190 ),
191 )
192 .menu(move |window, cx| {
193 Some(ContextMenu::build(window, cx, {
194 let entity = entity.clone();
195 move |this, _, _| {
196 this.entry("Launch", None, {
197 let entity = entity.clone();
198 move |_, cx| {
199 let _ =
200 entity.update(cx, |this, cx| {
201 this.spawn_mode =
202 SpawnMode::Launch;
203 cx.notify();
204 });
205 }
206 })
207 .entry("Attach", None, {
208 let entity = entity.clone();
209 move |_, cx| {
210 let _ =
211 entity.update(cx, |this, cx| {
212 this.spawn_mode =
213 SpawnMode::Attach;
214 cx.notify();
215 });
216 }
217 })
218 }
219 }))
220 })
221 .with_handle(self.popover_handle.clone())
222 .into_any_element(),
223 })
224 }),
225 ),
226 )
227 }
228}
229
230fn kind_for_label(label: &str) -> DebugAdapterKind {
231 match label {
232 "LLDB" => DebugAdapterKind::Lldb,
233 "Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
234 "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
235 "PHP" => DebugAdapterKind::Php(TCPHost::default()),
236 "Delve" => DebugAdapterKind::Go(TCPHost::default()),
237 _ => {
238 unimplemented!()
239 } // Maybe we should set a toast notification here
240 }
241}
242impl InertState {
243 fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
244 let settings = ThemeSettings::get_global(cx);
245 let text_style = TextStyle {
246 color: cx.theme().colors().text,
247 font_family: settings.buffer_font.family.clone(),
248 font_features: settings.buffer_font.features.clone(),
249 font_size: settings.buffer_font_size(cx).into(),
250 font_weight: settings.buffer_font.weight,
251 line_height: relative(settings.buffer_line_height.value()),
252 ..Default::default()
253 };
254
255 EditorElement::new(
256 editor,
257 EditorStyle {
258 background: cx.theme().colors().editor_background,
259 local_player: cx.theme().players().local(),
260 text: text_style,
261 ..Default::default()
262 },
263 )
264 }
265
266 fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
267 let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
268 let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
269 unimplemented!("Automatic selection of a debugger based on users project")
270 }));
271
272 let config = DebugAdapterConfig {
273 label: "hard coded attach".into(),
274 kind,
275 request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
276 program: None,
277 cwd: Some(cwd),
278 initialize_args: None,
279 supports_attach: true,
280 };
281
282 let _ = self.workspace.update(cx, |workspace, cx| {
283 let project = workspace.project().clone();
284 workspace.toggle_modal(window, cx, |window, cx| {
285 AttachModal::new(project, config, window, cx)
286 });
287 });
288 }
289}