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}