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