1use collections::FxHashMap;
2use language::LanguageRegistry;
3use paths::local_debug_file_relative_path;
4use std::{
5 borrow::Cow,
6 path::{Path, PathBuf},
7 sync::Arc,
8 usize,
9};
10use tasks_ui::{TaskOverrides, TasksModal};
11
12use dap::{
13 DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
14};
15use editor::{Editor, EditorElement, EditorStyle};
16use fuzzy::{StringMatch, StringMatchCandidate};
17use gpui::{
18 App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
19 InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
20 TextStyle, UnderlineStyle, WeakEntity,
21};
22use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
23use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
24use settings::{Settings, initial_local_debug_tasks_content};
25use task::{DebugScenario, RevealTarget, ZedDebugConfig};
26use theme::ThemeSettings;
27use ui::{
28 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
29 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
30 IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
31 ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
32 StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,
33 relative, rems, v_flex,
34};
35use util::ResultExt;
36use workspace::{ModalView, Workspace, pane};
37
38use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
39
40// enum SaveScenarioState {
41// Saving,
42// Saved((ProjectPath, SharedString)),
43// Failed(SharedString),
44// }
45
46pub(super) struct NewProcessModal {
47 workspace: WeakEntity<Workspace>,
48 debug_panel: WeakEntity<DebugPanel>,
49 mode: NewProcessMode,
50 debug_picker: Entity<Picker<DebugDelegate>>,
51 attach_mode: Entity<AttachMode>,
52 launch_mode: Entity<LaunchMode>,
53 task_mode: TaskMode,
54 debugger: Option<DebugAdapterName>,
55 // save_scenario_state: Option<SaveScenarioState>,
56 _subscriptions: [Subscription; 3],
57}
58
59fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
60 match request {
61 DebugRequest::Launch(config) => {
62 let last_path_component = Path::new(&config.program)
63 .file_name()
64 .map(|name| name.to_string_lossy())
65 .unwrap_or_else(|| Cow::Borrowed(&config.program));
66
67 format!("{} ({debugger})", last_path_component).into()
68 }
69 DebugRequest::Attach(config) => format!(
70 "pid: {} ({debugger})",
71 config.process_id.unwrap_or(u32::MAX)
72 )
73 .into(),
74 }
75}
76
77impl NewProcessModal {
78 pub(super) fn show(
79 workspace: &mut Workspace,
80 window: &mut Window,
81 mode: NewProcessMode,
82 reveal_target: Option<RevealTarget>,
83 cx: &mut Context<Workspace>,
84 ) {
85 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
86 return;
87 };
88 let task_store = workspace.project().read(cx).task_store().clone();
89 let languages = workspace.app_state().languages.clone();
90
91 cx.spawn_in(window, async move |workspace, cx| {
92 let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
93 tasks_ui::task_contexts(workspace, window, cx)
94 })?;
95 workspace.update_in(cx, |workspace, window, cx| {
96 let workspace_handle = workspace.weak_handle();
97 workspace.toggle_modal(window, cx, |window, cx| {
98 let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
99
100 let launch_picker = cx.new(|cx| {
101 let delegate =
102 DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
103 Picker::uniform_list(delegate, window, cx).modal(false)
104 });
105
106 let configure_mode = LaunchMode::new(window, cx);
107
108 let task_overrides = Some(TaskOverrides { reveal_target });
109
110 let task_mode = TaskMode {
111 task_modal: cx.new(|cx| {
112 TasksModal::new(
113 task_store.clone(),
114 Arc::new(TaskContexts::default()),
115 task_overrides,
116 false,
117 workspace_handle.clone(),
118 window,
119 cx,
120 )
121 }),
122 };
123
124 let _subscriptions = [
125 cx.subscribe(&launch_picker, |_, _, _, cx| {
126 cx.emit(DismissEvent);
127 }),
128 cx.subscribe(
129 &attach_mode.read(cx).attach_picker.clone(),
130 |_, _, _, cx| {
131 cx.emit(DismissEvent);
132 },
133 ),
134 cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| {
135 cx.emit(DismissEvent)
136 }),
137 ];
138
139 cx.spawn_in(window, {
140 let launch_picker = launch_picker.downgrade();
141 let configure_mode = configure_mode.downgrade();
142 let task_modal = task_mode.task_modal.downgrade();
143
144 async move |this, cx| {
145 let task_contexts = task_contexts.await;
146 let task_contexts = Arc::new(task_contexts);
147 launch_picker
148 .update_in(cx, |picker, window, cx| {
149 picker.delegate.task_contexts_loaded(
150 task_contexts.clone(),
151 languages,
152 window,
153 cx,
154 );
155 picker.refresh(window, cx);
156 cx.notify();
157 })
158 .ok();
159
160 if let Some(active_cwd) = task_contexts
161 .active_context()
162 .and_then(|context| context.cwd.clone())
163 {
164 configure_mode
165 .update_in(cx, |configure_mode, window, cx| {
166 configure_mode.load(active_cwd, window, cx);
167 })
168 .ok();
169 }
170
171 task_modal
172 .update_in(cx, |task_modal, window, cx| {
173 task_modal.task_contexts_loaded(task_contexts, window, cx);
174 })
175 .ok();
176
177 this.update(cx, |_, cx| {
178 cx.notify();
179 })
180 .ok();
181 }
182 })
183 .detach();
184
185 Self {
186 debug_picker: launch_picker,
187 attach_mode,
188 launch_mode: configure_mode,
189 task_mode,
190 debugger: None,
191 mode,
192 debug_panel: debug_panel.downgrade(),
193 workspace: workspace_handle,
194 // save_scenario_state: None,
195 _subscriptions,
196 }
197 });
198 })?;
199
200 anyhow::Ok(())
201 })
202 .detach();
203 }
204
205 fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
206 let dap_menu = self.adapter_drop_down_menu(window, cx);
207 match self.mode {
208 NewProcessMode::Task => self
209 .task_mode
210 .task_modal
211 .read(cx)
212 .picker
213 .clone()
214 .into_any_element(),
215 NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
216 this.clone().render(window, cx).into_any_element()
217 }),
218 NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
219 this.clone().render(dap_menu, window, cx).into_any_element()
220 }),
221 NewProcessMode::Debug => v_flex()
222 .w(rems(34.))
223 .child(self.debug_picker.clone())
224 .into_any_element(),
225 }
226 }
227
228 fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
229 match self.mode {
230 NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
231 NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
232 NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
233 NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
234 }
235 }
236
237 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
238 let request = match self.mode {
239 NewProcessMode::Launch => Some(DebugRequest::Launch(
240 self.launch_mode.read(cx).debug_request(cx),
241 )),
242 NewProcessMode::Attach => Some(DebugRequest::Attach(
243 self.attach_mode.read(cx).debug_request(),
244 )),
245 _ => None,
246 }?;
247 let label = suggested_label(&request, debugger);
248
249 let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
250 Some(self.launch_mode.read(cx).stop_on_entry.selected())
251 } else {
252 None
253 };
254
255 let session_scenario = ZedDebugConfig {
256 adapter: debugger.to_owned().into(),
257 label,
258 request: request,
259 stop_on_entry,
260 };
261
262 cx.global::<DapRegistry>()
263 .adapter(&session_scenario.adapter)
264 .and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
265 }
266
267 fn start_new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
268 if self.debugger.as_ref().is_none() {
269 return;
270 }
271
272 if let NewProcessMode::Debug = &self.mode {
273 self.debug_picker.update(cx, |picker, cx| {
274 picker.delegate.confirm(false, window, cx);
275 });
276 return;
277 }
278
279 // TODO: Restore once we have proper, comment preserving edits
280 // if let NewProcessMode::Launch = &self.mode {
281 // if self.launch_mode.read(cx).save_to_debug_json.selected() {
282 // self.save_debug_scenario(window, cx);
283 // }
284 // }
285
286 let Some(debugger) = self.debugger.as_ref() else {
287 return;
288 };
289
290 let Some(config) = self.debug_scenario(debugger, cx) else {
291 log::error!("debug config not found in mode: {}", self.mode);
292 return;
293 };
294
295 let debug_panel = self.debug_panel.clone();
296 let Some(task_contexts) = self.task_contexts(cx) else {
297 return;
298 };
299 send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
300 let task_context = task_contexts.active_context().cloned().unwrap_or_default();
301 let worktree_id = task_contexts.worktree();
302 cx.spawn_in(window, async move |this, cx| {
303 debug_panel.update_in(cx, |debug_panel, window, cx| {
304 debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
305 })?;
306 this.update(cx, |_, cx| {
307 cx.emit(DismissEvent);
308 })
309 .ok();
310 anyhow::Ok(())
311 })
312 .detach_and_log_err(cx);
313 }
314
315 fn update_attach_picker(
316 attach: &Entity<AttachMode>,
317 adapter: &DebugAdapterName,
318 window: &mut Window,
319 cx: &mut App,
320 ) {
321 attach.update(cx, |this, cx| {
322 if adapter.0 != this.definition.adapter {
323 this.definition.adapter = adapter.0.clone();
324
325 this.attach_picker.update(cx, |this, cx| {
326 this.picker.update(cx, |this, cx| {
327 this.delegate.definition.adapter = adapter.0.clone();
328 this.focus(window, cx);
329 })
330 });
331 }
332
333 cx.notify();
334 })
335 }
336
337 fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
338 self.debug_picker.read(cx).delegate.task_contexts.clone()
339 }
340
341 // fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
342 // let Some((save_scenario, scenario_label)) = self
343 // .debugger
344 // .as_ref()
345 // .and_then(|debugger| self.debug_scenario(&debugger, cx))
346 // .zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree()))
347 // .and_then(|(scenario, worktree_id)| {
348 // self.debug_panel
349 // .update(cx, |panel, cx| {
350 // panel.save_scenario(&scenario, worktree_id, window, cx)
351 // })
352 // .ok()
353 // .zip(Some(scenario.label.clone()))
354 // })
355 // else {
356 // return;
357 // };
358
359 // self.save_scenario_state = Some(SaveScenarioState::Saving);
360
361 // cx.spawn(async move |this, cx| {
362 // let res = save_scenario.await;
363
364 // this.update(cx, |this, _| match res {
365 // Ok(saved_file) => {
366 // this.save_scenario_state =
367 // Some(SaveScenarioState::Saved((saved_file, scenario_label)))
368 // }
369 // Err(error) => {
370 // this.save_scenario_state =
371 // Some(SaveScenarioState::Failed(error.to_string().into()))
372 // }
373 // })
374 // .ok();
375
376 // cx.background_executor().timer(Duration::from_secs(3)).await;
377 // this.update(cx, |this, _| this.save_scenario_state.take())
378 // .ok();
379 // })
380 // .detach();
381 // }
382
383 fn adapter_drop_down_menu(
384 &mut self,
385 window: &mut Window,
386 cx: &mut Context<Self>,
387 ) -> ui::DropdownMenu {
388 let workspace = self.workspace.clone();
389 let weak = cx.weak_entity();
390 let active_buffer = self.task_contexts(cx).and_then(|tc| {
391 tc.active_item_context
392 .as_ref()
393 .and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
394 });
395
396 let active_buffer_language = active_buffer
397 .and_then(|buffer| buffer.read(cx).language())
398 .cloned();
399
400 let mut available_adapters = workspace
401 .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
402 .unwrap_or_default();
403 if let Some(language) = active_buffer_language {
404 available_adapters.sort_by_key(|adapter| {
405 language
406 .config()
407 .debuggers
408 .get_index_of(adapter.0.as_ref())
409 .unwrap_or(usize::MAX)
410 });
411 }
412
413 if self.debugger.is_none() {
414 self.debugger = available_adapters.first().cloned();
415 }
416
417 let label = self
418 .debugger
419 .as_ref()
420 .map(|d| d.0.clone())
421 .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
422
423 DropdownMenu::new(
424 "dap-adapter-picker",
425 label,
426 ContextMenu::build(window, cx, move |mut menu, _, _| {
427 let setter_for_name = |name: DebugAdapterName| {
428 let weak = weak.clone();
429 move |window: &mut Window, cx: &mut App| {
430 weak.update(cx, |this, cx| {
431 this.debugger = Some(name.clone());
432 cx.notify();
433 if let NewProcessMode::Attach = &this.mode {
434 Self::update_attach_picker(&this.attach_mode, &name, window, cx);
435 }
436 })
437 .ok();
438 }
439 };
440
441 for adapter in available_adapters.into_iter() {
442 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
443 }
444
445 menu
446 }),
447 )
448 }
449
450 fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
451 let this = cx.entity();
452 window
453 .spawn(cx, async move |cx| {
454 let worktree_id = this.update(cx, |this, cx| {
455 let tcx = this.task_contexts(cx);
456 tcx?.worktree()
457 })?;
458
459 let Some(worktree_id) = worktree_id else {
460 let _ = cx.prompt(
461 PromptLevel::Critical,
462 "Cannot open debug.json",
463 Some("You must have at least one project open"),
464 &[PromptButton::ok("Ok")],
465 );
466 return Ok(());
467 };
468
469 let editor = this
470 .update_in(cx, |this, window, cx| {
471 this.workspace.update(cx, |workspace, cx| {
472 workspace.open_path(
473 ProjectPath {
474 worktree_id,
475 path: local_debug_file_relative_path().into(),
476 },
477 None,
478 true,
479 window,
480 cx,
481 )
482 })
483 })??
484 .await?;
485
486 cx.update(|_window, cx| {
487 if let Some(editor) = editor.act_as::<Editor>(cx) {
488 editor.update(cx, |editor, cx| {
489 editor.buffer().update(cx, |buffer, cx| {
490 if let Some(singleton) = buffer.as_singleton() {
491 singleton.update(cx, |buffer, cx| {
492 if buffer.is_empty() {
493 buffer.edit(
494 [(0..0, initial_local_debug_tasks_content())],
495 None,
496 cx,
497 );
498 }
499 })
500 }
501 })
502 });
503 }
504 })
505 .ok();
506
507 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
508
509 anyhow::Ok(())
510 })
511 .detach();
512 }
513}
514
515static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
516
517#[derive(Clone)]
518pub(crate) enum NewProcessMode {
519 Task,
520 Launch,
521 Attach,
522 Debug,
523}
524
525impl std::fmt::Display for NewProcessMode {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 let mode = match self {
528 NewProcessMode::Task => "Run",
529 NewProcessMode::Debug => "Debug",
530 NewProcessMode::Attach => "Attach",
531 NewProcessMode::Launch => "Launch",
532 };
533
534 write!(f, "{}", mode)
535 }
536}
537
538impl Focusable for NewProcessMode {
539 fn focus_handle(&self, cx: &App) -> FocusHandle {
540 cx.focus_handle()
541 }
542}
543
544fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
545 let settings = ThemeSettings::get_global(cx);
546 let theme = cx.theme();
547
548 let text_style = TextStyle {
549 color: cx.theme().colors().text,
550 font_family: settings.buffer_font.family.clone(),
551 font_features: settings.buffer_font.features.clone(),
552 font_size: settings.buffer_font_size(cx).into(),
553 font_weight: settings.buffer_font.weight,
554 line_height: relative(settings.buffer_line_height.value()),
555 background_color: Some(theme.colors().editor_background),
556 ..Default::default()
557 };
558
559 let element = EditorElement::new(
560 editor,
561 EditorStyle {
562 background: theme.colors().editor_background,
563 local_player: theme.players().local(),
564 text: text_style,
565 ..Default::default()
566 },
567 );
568
569 div()
570 .rounded_md()
571 .p_1()
572 .border_1()
573 .border_color(theme.colors().border_variant)
574 .when(
575 editor.focus_handle(cx).contains_focused(window, cx),
576 |this| this.border_color(theme.colors().border_focused),
577 )
578 .child(element)
579 .bg(theme.colors().editor_background)
580}
581
582impl Render for NewProcessModal {
583 fn render(
584 &mut self,
585 window: &mut ui::Window,
586 cx: &mut ui::Context<Self>,
587 ) -> impl ui::IntoElement {
588 v_flex()
589 .size_full()
590 .w(rems(34.))
591 .key_context({
592 let mut key_context = KeyContext::new_with_defaults();
593 key_context.add("Pane");
594 key_context.add("RunModal");
595 key_context
596 })
597 .elevation_3(cx)
598 .bg(cx.theme().colors().elevated_surface_background)
599 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
600 cx.emit(DismissEvent);
601 }))
602 .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
603 this.mode = match this.mode {
604 NewProcessMode::Task => NewProcessMode::Debug,
605 NewProcessMode::Debug => NewProcessMode::Attach,
606 NewProcessMode::Attach => NewProcessMode::Launch,
607 NewProcessMode::Launch => NewProcessMode::Task,
608 };
609
610 this.mode_focus_handle(cx).focus(window);
611 }))
612 .on_action(
613 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
614 this.mode = match this.mode {
615 NewProcessMode::Task => NewProcessMode::Launch,
616 NewProcessMode::Debug => NewProcessMode::Task,
617 NewProcessMode::Attach => NewProcessMode::Debug,
618 NewProcessMode::Launch => NewProcessMode::Attach,
619 };
620
621 this.mode_focus_handle(cx).focus(window);
622 }),
623 )
624 .child(
625 h_flex()
626 .w_full()
627 .justify_around()
628 .p_2()
629 .child(
630 h_flex()
631 .justify_start()
632 .w_full()
633 .child(
634 ToggleButton::new(
635 "debugger-session-ui-tasks-button",
636 NewProcessMode::Task.to_string(),
637 )
638 .size(ButtonSize::Default)
639 .toggle_state(matches!(self.mode, NewProcessMode::Task))
640 .style(ui::ButtonStyle::Subtle)
641 .on_click(cx.listener(|this, _, window, cx| {
642 this.mode = NewProcessMode::Task;
643 this.mode_focus_handle(cx).focus(window);
644 cx.notify();
645 }))
646 .tooltip(Tooltip::text("Run predefined task"))
647 .first(),
648 )
649 .child(
650 ToggleButton::new(
651 "debugger-session-ui-launch-button",
652 NewProcessMode::Debug.to_string(),
653 )
654 .size(ButtonSize::Default)
655 .style(ui::ButtonStyle::Subtle)
656 .toggle_state(matches!(self.mode, NewProcessMode::Debug))
657 .on_click(cx.listener(|this, _, window, cx| {
658 this.mode = NewProcessMode::Debug;
659 this.mode_focus_handle(cx).focus(window);
660 cx.notify();
661 }))
662 .tooltip(Tooltip::text("Start a predefined debug scenario"))
663 .middle(),
664 )
665 .child(
666 ToggleButton::new(
667 "debugger-session-ui-attach-button",
668 NewProcessMode::Attach.to_string(),
669 )
670 .size(ButtonSize::Default)
671 .toggle_state(matches!(self.mode, NewProcessMode::Attach))
672 .style(ui::ButtonStyle::Subtle)
673 .on_click(cx.listener(|this, _, window, cx| {
674 this.mode = NewProcessMode::Attach;
675
676 if let Some(debugger) = this.debugger.as_ref() {
677 Self::update_attach_picker(
678 &this.attach_mode,
679 &debugger,
680 window,
681 cx,
682 );
683 }
684 this.mode_focus_handle(cx).focus(window);
685 cx.notify();
686 }))
687 .tooltip(Tooltip::text("Attach the debugger to a running process"))
688 .middle(),
689 )
690 .child(
691 ToggleButton::new(
692 "debugger-session-ui-custom-button",
693 NewProcessMode::Launch.to_string(),
694 )
695 .size(ButtonSize::Default)
696 .toggle_state(matches!(self.mode, NewProcessMode::Launch))
697 .style(ui::ButtonStyle::Subtle)
698 .on_click(cx.listener(|this, _, window, cx| {
699 this.mode = NewProcessMode::Launch;
700 this.mode_focus_handle(cx).focus(window);
701 cx.notify();
702 }))
703 .tooltip(Tooltip::text("Launch a new process with a debugger"))
704 .last(),
705 ),
706 )
707 .justify_between()
708 .border_color(cx.theme().colors().border_variant)
709 .border_b_1(),
710 )
711 .child(v_flex().child(self.render_mode(window, cx)))
712 .map(|el| {
713 let container = h_flex()
714 .justify_between()
715 .gap_2()
716 .p_2()
717 .border_color(cx.theme().colors().border_variant)
718 .border_t_1()
719 .w_full();
720 match self.mode {
721 NewProcessMode::Launch => el.child(
722 container
723 .child(
724 h_flex()
725 .text_ui_sm(cx)
726 .text_color(Color::Muted.color(cx))
727 .child(
728 InteractiveText::new(
729 "open-debug-json",
730 StyledText::new(
731 "Open .zed/debug.json for advanced configuration",
732 )
733 .with_highlights([(
734 5..20,
735 HighlightStyle {
736 underline: Some(UnderlineStyle {
737 thickness: px(1.0),
738 color: None,
739 wavy: false,
740 }),
741 ..Default::default()
742 },
743 )]),
744 )
745 .on_click(
746 vec![5..20],
747 {
748 let this = cx.entity();
749 move |_, window, cx| {
750 this.update(cx, |this, cx| {
751 this.open_debug_json(window, cx);
752 })
753 }
754 },
755 ),
756 ),
757 )
758 .child(
759 Button::new("debugger-spawn", "Start")
760 .on_click(cx.listener(|this, _, window, cx| {
761 this.start_new_session(window, cx)
762 }))
763 .disabled(
764 self.debugger.is_none()
765 || self
766 .launch_mode
767 .read(cx)
768 .program
769 .read(cx)
770 .is_empty(cx),
771 ),
772 ),
773 ),
774 NewProcessMode::Attach => el.child(
775 container
776 .child(div().child(self.adapter_drop_down_menu(window, cx)))
777 .child(
778 Button::new("debugger-spawn", "Start")
779 .on_click(cx.listener(|this, _, window, cx| {
780 this.start_new_session(window, cx)
781 }))
782 .disabled(
783 self.debugger.is_none()
784 || self
785 .attach_mode
786 .read(cx)
787 .attach_picker
788 .read(cx)
789 .picker
790 .read(cx)
791 .delegate
792 .match_count()
793 == 0,
794 ),
795 ),
796 ),
797 NewProcessMode::Debug => el,
798 NewProcessMode::Task => el,
799 }
800 })
801 }
802}
803
804impl EventEmitter<DismissEvent> for NewProcessModal {}
805impl Focusable for NewProcessModal {
806 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
807 self.mode_focus_handle(cx)
808 }
809}
810
811impl ModalView for NewProcessModal {}
812
813impl RenderOnce for AttachMode {
814 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
815 v_flex()
816 .w_full()
817 .track_focus(&self.attach_picker.focus_handle(cx))
818 .child(self.attach_picker.clone())
819 }
820}
821
822#[derive(Clone)]
823pub(super) struct LaunchMode {
824 program: Entity<Editor>,
825 cwd: Entity<Editor>,
826 stop_on_entry: ToggleState,
827 // save_to_debug_json: ToggleState,
828}
829
830impl LaunchMode {
831 pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
832 let program = cx.new(|cx| Editor::single_line(window, cx));
833 program.update(cx, |this, cx| {
834 this.set_placeholder_text("ENV=Zed ~/bin/debugger --launch", cx);
835 });
836
837 let cwd = cx.new(|cx| Editor::single_line(window, cx));
838 cwd.update(cx, |this, cx| {
839 this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", cx);
840 });
841
842 cx.new(|_| Self {
843 program,
844 cwd,
845 stop_on_entry: ToggleState::Unselected,
846 // save_to_debug_json: ToggleState::Unselected,
847 })
848 }
849
850 fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
851 self.cwd.update(cx, |editor, cx| {
852 if editor.is_empty(cx) {
853 editor.set_text(cwd.to_string_lossy(), window, cx);
854 }
855 });
856 }
857
858 pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
859 let cwd_text = self.cwd.read(cx).text(cx);
860 let cwd = if cwd_text.is_empty() {
861 None
862 } else {
863 Some(PathBuf::from(cwd_text))
864 };
865
866 if cfg!(windows) {
867 return task::LaunchRequest {
868 program: self.program.read(cx).text(cx),
869 cwd,
870 args: Default::default(),
871 env: Default::default(),
872 };
873 }
874 let command = self.program.read(cx).text(cx);
875 let mut args = shlex::split(&command).into_iter().flatten().peekable();
876 let mut env = FxHashMap::default();
877 while args.peek().is_some_and(|arg| arg.contains('=')) {
878 let arg = args.next().unwrap();
879 let (lhs, rhs) = arg.split_once('=').unwrap();
880 env.insert(lhs.to_string(), rhs.to_string());
881 }
882
883 let program = if let Some(program) = args.next() {
884 program
885 } else {
886 env = FxHashMap::default();
887 command
888 };
889
890 let args = args.collect::<Vec<_>>();
891
892 task::LaunchRequest {
893 program,
894 cwd,
895 args,
896 env,
897 }
898 }
899
900 fn render(
901 &mut self,
902 adapter_menu: DropdownMenu,
903 window: &mut Window,
904 cx: &mut ui::Context<Self>,
905 ) -> impl IntoElement {
906 v_flex()
907 .p_2()
908 .w_full()
909 .gap_3()
910 .track_focus(&self.program.focus_handle(cx))
911 .child(
912 h_flex()
913 .child(
914 Label::new("Debugger")
915 .size(ui::LabelSize::Small)
916 .color(Color::Muted),
917 )
918 .gap(ui::DynamicSpacing::Base08.rems(cx))
919 .child(adapter_menu),
920 )
921 .child(
922 Label::new("Debugger Program")
923 .size(ui::LabelSize::Small)
924 .color(Color::Muted),
925 )
926 .child(render_editor(&self.program, window, cx))
927 .child(
928 Label::new("Working Directory")
929 .size(ui::LabelSize::Small)
930 .color(Color::Muted),
931 )
932 .child(render_editor(&self.cwd, window, cx))
933 .child(
934 CheckboxWithLabel::new(
935 "debugger-stop-on-entry",
936 Label::new("Stop on Entry")
937 .size(ui::LabelSize::Small)
938 .color(Color::Muted),
939 self.stop_on_entry,
940 {
941 let this = cx.weak_entity();
942 move |state, _, cx| {
943 this.update(cx, |this, _| {
944 this.stop_on_entry = *state;
945 })
946 .ok();
947 }
948 },
949 )
950 .checkbox_position(ui::IconPosition::End),
951 )
952 // TODO: restore once we have proper, comment preserving
953 // file edits.
954 // .child(
955 // CheckboxWithLabel::new(
956 // "debugger-save-to-debug-json",
957 // Label::new("Save to debug.json")
958 // .size(ui::LabelSize::Small)
959 // .color(Color::Muted),
960 // self.save_to_debug_json,
961 // {
962 // let this = cx.weak_entity();
963 // move |state, _, cx| {
964 // this.update(cx, |this, _| {
965 // this.save_to_debug_json = *state;
966 // })
967 // .ok();
968 // }
969 // },
970 // )
971 // .checkbox_position(ui::IconPosition::End),
972 // )
973 }
974}
975
976#[derive(Clone)]
977pub(super) struct AttachMode {
978 pub(super) definition: ZedDebugConfig,
979 pub(super) attach_picker: Entity<AttachModal>,
980}
981
982impl AttachMode {
983 pub(super) fn new(
984 debugger: Option<DebugAdapterName>,
985 workspace: WeakEntity<Workspace>,
986 window: &mut Window,
987 cx: &mut Context<NewProcessModal>,
988 ) -> Entity<Self> {
989 let definition = ZedDebugConfig {
990 adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
991 label: "Attach New Session Setup".into(),
992 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
993 stop_on_entry: Some(false),
994 };
995 let attach_picker = cx.new(|cx| {
996 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
997 window.focus(&modal.focus_handle(cx));
998
999 modal
1000 });
1001
1002 cx.new(|_| Self {
1003 definition,
1004 attach_picker,
1005 })
1006 }
1007 pub(super) fn debug_request(&self) -> task::AttachRequest {
1008 task::AttachRequest { process_id: None }
1009 }
1010}
1011
1012#[derive(Clone)]
1013pub(super) struct TaskMode {
1014 pub(super) task_modal: Entity<TasksModal>,
1015}
1016
1017pub(super) struct DebugDelegate {
1018 task_store: Entity<TaskStore>,
1019 candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
1020 selected_index: usize,
1021 matches: Vec<StringMatch>,
1022 prompt: String,
1023 debug_panel: WeakEntity<DebugPanel>,
1024 task_contexts: Option<Arc<TaskContexts>>,
1025 divider_index: Option<usize>,
1026 last_used_candidate_index: Option<usize>,
1027}
1028
1029impl DebugDelegate {
1030 pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
1031 Self {
1032 task_store,
1033 candidates: Vec::default(),
1034 selected_index: 0,
1035 matches: Vec::new(),
1036 prompt: String::new(),
1037 debug_panel,
1038 task_contexts: None,
1039 divider_index: None,
1040 last_used_candidate_index: None,
1041 }
1042 }
1043
1044 fn get_scenario_kind(
1045 languages: &Arc<LanguageRegistry>,
1046 dap_registry: &DapRegistry,
1047 scenario: DebugScenario,
1048 ) -> (Option<TaskSourceKind>, DebugScenario) {
1049 let language_names = languages.language_names();
1050 let language = dap_registry
1051 .adapter_language(&scenario.adapter)
1052 .map(|language| TaskSourceKind::Language {
1053 name: language.into(),
1054 });
1055
1056 let language = language.or_else(|| {
1057 scenario.label.split_whitespace().find_map(|word| {
1058 language_names
1059 .iter()
1060 .find(|name| name.eq_ignore_ascii_case(word))
1061 .map(|name| TaskSourceKind::Language {
1062 name: name.to_owned().into(),
1063 })
1064 })
1065 });
1066
1067 (language, scenario)
1068 }
1069
1070 pub fn task_contexts_loaded(
1071 &mut self,
1072 task_contexts: Arc<TaskContexts>,
1073 languages: Arc<LanguageRegistry>,
1074 _window: &mut Window,
1075 cx: &mut Context<Picker<Self>>,
1076 ) {
1077 self.task_contexts = Some(task_contexts);
1078
1079 let (recent, scenarios) = self
1080 .task_store
1081 .update(cx, |task_store, cx| {
1082 task_store.task_inventory().map(|inventory| {
1083 inventory.update(cx, |inventory, cx| {
1084 inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
1085 })
1086 })
1087 })
1088 .unwrap_or_default();
1089
1090 if !recent.is_empty() {
1091 self.last_used_candidate_index = Some(recent.len() - 1);
1092 }
1093
1094 let dap_registry = cx.global::<DapRegistry>();
1095
1096 self.candidates = recent
1097 .into_iter()
1098 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1099 .chain(scenarios.into_iter().map(|(kind, scenario)| {
1100 let (language, scenario) =
1101 Self::get_scenario_kind(&languages, &dap_registry, scenario);
1102 (language.or(Some(kind)), scenario)
1103 }))
1104 .collect();
1105 }
1106}
1107
1108impl PickerDelegate for DebugDelegate {
1109 type ListItem = ui::ListItem;
1110
1111 fn match_count(&self) -> usize {
1112 self.matches.len()
1113 }
1114
1115 fn selected_index(&self) -> usize {
1116 self.selected_index
1117 }
1118
1119 fn set_selected_index(
1120 &mut self,
1121 ix: usize,
1122 _window: &mut Window,
1123 _cx: &mut Context<picker::Picker<Self>>,
1124 ) {
1125 self.selected_index = ix;
1126 }
1127
1128 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1129 "".into()
1130 }
1131
1132 fn update_matches(
1133 &mut self,
1134 query: String,
1135 window: &mut Window,
1136 cx: &mut Context<picker::Picker<Self>>,
1137 ) -> gpui::Task<()> {
1138 let candidates = self.candidates.clone();
1139
1140 cx.spawn_in(window, async move |picker, cx| {
1141 let candidates: Vec<_> = candidates
1142 .into_iter()
1143 .enumerate()
1144 .map(|(index, (_, candidate))| {
1145 StringMatchCandidate::new(index, candidate.label.as_ref())
1146 })
1147 .collect();
1148
1149 let matches = fuzzy::match_strings(
1150 &candidates,
1151 &query,
1152 true,
1153 1000,
1154 &Default::default(),
1155 cx.background_executor().clone(),
1156 )
1157 .await;
1158
1159 picker
1160 .update(cx, |picker, _| {
1161 let delegate = &mut picker.delegate;
1162
1163 delegate.matches = matches;
1164 delegate.prompt = query;
1165
1166 delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1167 let index = delegate
1168 .matches
1169 .partition_point(|matching_task| matching_task.candidate_id <= index);
1170 Some(index).and_then(|index| (index != 0).then(|| index - 1))
1171 });
1172
1173 if delegate.matches.is_empty() {
1174 delegate.selected_index = 0;
1175 } else {
1176 delegate.selected_index =
1177 delegate.selected_index.min(delegate.matches.len() - 1);
1178 }
1179 })
1180 .log_err();
1181 })
1182 }
1183
1184 fn separators_after_indices(&self) -> Vec<usize> {
1185 if let Some(i) = self.divider_index {
1186 vec![i]
1187 } else {
1188 Vec::new()
1189 }
1190 }
1191
1192 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1193 let debug_scenario = self
1194 .matches
1195 .get(self.selected_index())
1196 .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1197
1198 let Some((_, debug_scenario)) = debug_scenario else {
1199 return;
1200 };
1201
1202 let (task_context, worktree_id) = self
1203 .task_contexts
1204 .as_ref()
1205 .and_then(|task_contexts| {
1206 Some((
1207 task_contexts.active_context().cloned()?,
1208 task_contexts.worktree(),
1209 ))
1210 })
1211 .unwrap_or_default();
1212
1213 send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1214 self.debug_panel
1215 .update(cx, |panel, cx| {
1216 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1217 })
1218 .ok();
1219
1220 cx.emit(DismissEvent);
1221 }
1222
1223 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1224 cx.emit(DismissEvent);
1225 }
1226
1227 fn render_match(
1228 &self,
1229 ix: usize,
1230 selected: bool,
1231 window: &mut Window,
1232 cx: &mut Context<picker::Picker<Self>>,
1233 ) -> Option<Self::ListItem> {
1234 let hit = &self.matches[ix];
1235
1236 let highlighted_location = HighlightedMatch {
1237 text: hit.string.clone(),
1238 highlight_positions: hit.positions.clone(),
1239 char_count: hit.string.chars().count(),
1240 color: Color::Default,
1241 };
1242 let task_kind = &self.candidates[hit.candidate_id].0;
1243
1244 let icon = match task_kind {
1245 Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1246 Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1247 Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1248 Some(TaskSourceKind::Lsp {
1249 language_name: name,
1250 ..
1251 })
1252 | Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1253 .get_icon_for_type(&name.to_lowercase(), cx)
1254 .map(Icon::from_path),
1255 None => Some(Icon::new(IconName::HistoryRerun)),
1256 }
1257 .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
1258 let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
1259 Some(Indicator::icon(
1260 Icon::new(IconName::BoltFilled).color(Color::Muted),
1261 ))
1262 } else {
1263 None
1264 };
1265 let icon = icon.map(|icon| IconWithIndicator::new(icon, indicator));
1266
1267 Some(
1268 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1269 .inset(true)
1270 .start_slot::<IconWithIndicator>(icon)
1271 .spacing(ListItemSpacing::Sparse)
1272 .toggle_state(selected)
1273 .child(highlighted_location.render(window, cx)),
1274 )
1275 }
1276}
1277
1278pub(crate) fn resolve_path(path: &mut String) {
1279 if path.starts_with('~') {
1280 let home = paths::home_dir().to_string_lossy().to_string();
1281 let trimmed_path = path.trim().to_owned();
1282 *path = trimmed_path.replacen('~', &home, 1);
1283 } else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
1284 *path = format!(
1285 "$ZED_WORKTREE_ROOT{}{}",
1286 std::path::MAIN_SEPARATOR,
1287 &strip_path
1288 );
1289 };
1290}
1291
1292#[cfg(test)]
1293impl NewProcessModal {
1294 // #[cfg(test)]
1295 // pub(crate) fn set_configure(
1296 // &mut self,
1297 // program: impl AsRef<str>,
1298 // cwd: impl AsRef<str>,
1299 // stop_on_entry: bool,
1300 // window: &mut Window,
1301 // cx: &mut Context<Self>,
1302 // ) {
1303 // self.mode = NewProcessMode::Launch;
1304 // self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
1305
1306 // self.launch_mode.update(cx, |configure, cx| {
1307 // configure.program.update(cx, |editor, cx| {
1308 // editor.clear(window, cx);
1309 // editor.set_text(program.as_ref(), window, cx);
1310 // });
1311
1312 // configure.cwd.update(cx, |editor, cx| {
1313 // editor.clear(window, cx);
1314 // editor.set_text(cwd.as_ref(), window, cx);
1315 // });
1316
1317 // configure.stop_on_entry = match stop_on_entry {
1318 // true => ToggleState::Selected,
1319 // _ => ToggleState::Unselected,
1320 // }
1321 // })
1322 // }
1323
1324 // pub(crate) fn save_scenario(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
1325 // self.save_debug_scenario(window, cx);
1326 // }
1327}