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}