1use collections::FxHashMap;
2use std::{
3 borrow::Cow,
4 ops::Not,
5 path::{Path, PathBuf},
6 sync::Arc,
7 time::Duration,
8 usize,
9};
10
11use anyhow::Result;
12use dap::{
13 DapRegistry, DebugRequest,
14 adapters::{DebugAdapterName, DebugTaskDefinition},
15};
16use editor::{Editor, EditorElement, EditorStyle};
17use fuzzy::{StringMatch, StringMatchCandidate};
18use gpui::{
19 Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
20 Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
21};
22use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
23use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
24use settings::Settings;
25use task::{DebugScenario, LaunchRequest};
26use theme::ThemeSettings;
27use ui::{
28 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
29 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
30 InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
31 ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
32 Toggleable, Window, div, h_flex, relative, rems, v_flex,
33};
34use util::ResultExt;
35use workspace::{ModalView, Workspace, pane};
36
37use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
38
39enum SaveScenarioState {
40 Saving,
41 Saved(ProjectPath),
42 Failed(SharedString),
43}
44
45pub(super) struct NewSessionModal {
46 workspace: WeakEntity<Workspace>,
47 debug_panel: WeakEntity<DebugPanel>,
48 mode: NewSessionMode,
49 launch_picker: Entity<Picker<DebugScenarioDelegate>>,
50 attach_mode: Entity<AttachMode>,
51 custom_mode: Entity<CustomMode>,
52 debugger: Option<DebugAdapterName>,
53 task_contexts: Arc<TaskContexts>,
54 save_scenario_state: Option<SaveScenarioState>,
55 _subscriptions: [Subscription; 2],
56}
57
58fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
59 match request {
60 DebugRequest::Launch(config) => {
61 let last_path_component = Path::new(&config.program)
62 .file_name()
63 .map(|name| name.to_string_lossy())
64 .unwrap_or_else(|| Cow::Borrowed(&config.program));
65
66 format!("{} ({debugger})", last_path_component).into()
67 }
68 DebugRequest::Attach(config) => format!(
69 "pid: {} ({debugger})",
70 config.process_id.unwrap_or(u32::MAX)
71 )
72 .into(),
73 }
74}
75
76impl NewSessionModal {
77 pub(super) fn show(
78 workspace: &mut Workspace,
79 window: &mut Window,
80 cx: &mut Context<Workspace>,
81 ) {
82 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
83 return;
84 };
85 let task_store = workspace.project().read(cx).task_store().clone();
86
87 cx.spawn_in(window, async move |workspace, cx| {
88 let task_contexts = Arc::from(
89 workspace
90 .update_in(cx, |workspace, window, cx| {
91 tasks_ui::task_contexts(workspace, window, cx)
92 })?
93 .await,
94 );
95
96 workspace.update_in(cx, |workspace, window, cx| {
97 let workspace_handle = workspace.weak_handle();
98 workspace.toggle_modal(window, cx, |window, cx| {
99 let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
100
101 let launch_picker = cx.new(|cx| {
102 Picker::uniform_list(
103 DebugScenarioDelegate::new(
104 debug_panel.downgrade(),
105 workspace_handle.clone(),
106 task_store,
107 task_contexts.clone(),
108 ),
109 window,
110 cx,
111 )
112 .modal(false)
113 });
114
115 let _subscriptions = [
116 cx.subscribe(&launch_picker, |_, _, _, cx| {
117 cx.emit(DismissEvent);
118 }),
119 cx.subscribe(
120 &attach_mode.read(cx).attach_picker.clone(),
121 |_, _, _, cx| {
122 cx.emit(DismissEvent);
123 },
124 ),
125 ];
126
127 let custom_mode = CustomMode::new(None, window, cx);
128
129 Self {
130 launch_picker,
131 attach_mode,
132 custom_mode,
133 debugger: None,
134 mode: NewSessionMode::Launch,
135 debug_panel: debug_panel.downgrade(),
136 workspace: workspace_handle,
137 task_contexts,
138 save_scenario_state: None,
139 _subscriptions,
140 }
141 });
142 })?;
143
144 anyhow::Ok(())
145 })
146 .detach();
147 }
148
149 fn render_mode(&self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
150 let dap_menu = self.adapter_drop_down_menu(window, cx);
151 match self.mode {
152 NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
153 this.clone().render(window, cx).into_any_element()
154 }),
155 NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
156 this.clone().render(dap_menu, window, cx).into_any_element()
157 }),
158 NewSessionMode::Launch => v_flex()
159 .w(rems(34.))
160 .child(self.launch_picker.clone())
161 .into_any_element(),
162 }
163 }
164
165 fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
166 match self.mode {
167 NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
168 NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
169 NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
170 }
171 }
172
173 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
174 let request = match self.mode {
175 NewSessionMode::Custom => Some(DebugRequest::Launch(
176 self.custom_mode.read(cx).debug_request(cx),
177 )),
178 NewSessionMode::Attach => Some(DebugRequest::Attach(
179 self.attach_mode.read(cx).debug_request(),
180 )),
181 _ => None,
182 }?;
183 let label = suggested_label(&request, debugger);
184
185 let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
186 Some(self.custom_mode.read(cx).stop_on_entry.selected())
187 } else {
188 None
189 };
190
191 Some(DebugScenario {
192 adapter: debugger.to_owned().into(),
193 label,
194 request: Some(request),
195 initialize_args: None,
196 tcp_connection: None,
197 stop_on_entry,
198 build: None,
199 })
200 }
201
202 fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
203 let Some(debugger) = self.debugger.as_ref() else {
204 // todo(debugger): show in UI.
205 log::error!("No debugger selected");
206 return;
207 };
208
209 if let NewSessionMode::Launch = &self.mode {
210 self.launch_picker.update(cx, |picker, cx| {
211 picker.delegate.confirm(false, window, cx);
212 });
213 return;
214 }
215
216 let Some(config) = self.debug_scenario(debugger, cx) else {
217 log::error!("debug config not found in mode: {}", self.mode);
218 return;
219 };
220
221 let debug_panel = self.debug_panel.clone();
222 let task_contexts = self.task_contexts.clone();
223 cx.spawn_in(window, async move |this, cx| {
224 let task_context = task_contexts.active_context().cloned().unwrap_or_default();
225 let worktree_id = task_contexts.worktree();
226 debug_panel.update_in(cx, |debug_panel, window, cx| {
227 debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
228 })?;
229 this.update(cx, |_, cx| {
230 cx.emit(DismissEvent);
231 })
232 .ok();
233 Result::<_, anyhow::Error>::Ok(())
234 })
235 .detach_and_log_err(cx);
236 }
237
238 fn update_attach_picker(
239 attach: &Entity<AttachMode>,
240 adapter: &DebugAdapterName,
241 window: &mut Window,
242 cx: &mut App,
243 ) {
244 attach.update(cx, |this, cx| {
245 if adapter != &this.definition.adapter {
246 this.definition.adapter = adapter.clone();
247
248 this.attach_picker.update(cx, |this, cx| {
249 this.picker.update(cx, |this, cx| {
250 this.delegate.definition.adapter = adapter.clone();
251 this.focus(window, cx);
252 })
253 });
254 }
255
256 cx.notify();
257 })
258 }
259 fn adapter_drop_down_menu(
260 &self,
261 window: &mut Window,
262 cx: &mut Context<Self>,
263 ) -> ui::DropdownMenu {
264 let workspace = self.workspace.clone();
265 let weak = cx.weak_entity();
266 let label = self
267 .debugger
268 .as_ref()
269 .map(|d| d.0.clone())
270 .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
271 let active_buffer_language = self
272 .task_contexts
273 .active_item_context
274 .as_ref()
275 .and_then(|item| {
276 item.1
277 .as_ref()
278 .and_then(|location| location.buffer.read(cx).language())
279 })
280 .cloned();
281
282 DropdownMenu::new(
283 "dap-adapter-picker",
284 label,
285 ContextMenu::build(window, cx, move |mut menu, _, cx| {
286 let setter_for_name = |name: DebugAdapterName| {
287 let weak = weak.clone();
288 move |window: &mut Window, cx: &mut App| {
289 weak.update(cx, |this, cx| {
290 this.debugger = Some(name.clone());
291 cx.notify();
292 if let NewSessionMode::Attach = &this.mode {
293 Self::update_attach_picker(&this.attach_mode, &name, window, cx);
294 }
295 })
296 .ok();
297 }
298 };
299
300 let mut available_adapters = workspace
301 .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
302 .unwrap_or_default();
303 if let Some(language) = active_buffer_language {
304 available_adapters.sort_by_key(|adapter| {
305 language
306 .config()
307 .debuggers
308 .get_index_of(adapter.0.as_ref())
309 .unwrap_or(usize::MAX)
310 });
311 }
312
313 for adapter in available_adapters.into_iter() {
314 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
315 }
316 menu
317 }),
318 )
319 }
320}
321
322static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
323
324#[derive(Clone)]
325enum NewSessionMode {
326 Custom,
327 Attach,
328 Launch,
329}
330
331impl std::fmt::Display for NewSessionMode {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 let mode = match self {
334 NewSessionMode::Launch => "Launch".to_owned(),
335 NewSessionMode::Attach => "Attach".to_owned(),
336 NewSessionMode::Custom => "Custom".to_owned(),
337 };
338
339 write!(f, "{}", mode)
340 }
341}
342
343impl Focusable for NewSessionMode {
344 fn focus_handle(&self, cx: &App) -> FocusHandle {
345 cx.focus_handle()
346 }
347}
348
349fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
350 let settings = ThemeSettings::get_global(cx);
351 let theme = cx.theme();
352
353 let text_style = TextStyle {
354 color: cx.theme().colors().text,
355 font_family: settings.buffer_font.family.clone(),
356 font_features: settings.buffer_font.features.clone(),
357 font_size: settings.buffer_font_size(cx).into(),
358 font_weight: settings.buffer_font.weight,
359 line_height: relative(settings.buffer_line_height.value()),
360 background_color: Some(theme.colors().editor_background),
361 ..Default::default()
362 };
363
364 let element = EditorElement::new(
365 editor,
366 EditorStyle {
367 background: theme.colors().editor_background,
368 local_player: theme.players().local(),
369 text: text_style,
370 ..Default::default()
371 },
372 );
373
374 div()
375 .rounded_md()
376 .p_1()
377 .border_1()
378 .border_color(theme.colors().border_variant)
379 .when(
380 editor.focus_handle(cx).contains_focused(window, cx),
381 |this| this.border_color(theme.colors().border_focused),
382 )
383 .child(element)
384 .bg(theme.colors().editor_background)
385}
386
387impl Render for NewSessionModal {
388 fn render(
389 &mut self,
390 window: &mut ui::Window,
391 cx: &mut ui::Context<Self>,
392 ) -> impl ui::IntoElement {
393 let this = cx.weak_entity().clone();
394
395 v_flex()
396 .size_full()
397 .w(rems(34.))
398 .key_context("Pane")
399 .elevation_3(cx)
400 .bg(cx.theme().colors().elevated_surface_background)
401 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
402 cx.emit(DismissEvent);
403 }))
404 .on_action(
405 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
406 this.mode = match this.mode {
407 NewSessionMode::Attach => NewSessionMode::Launch,
408 NewSessionMode::Launch => NewSessionMode::Attach,
409 _ => {
410 return;
411 }
412 };
413
414 this.mode_focus_handle(cx).focus(window);
415 }),
416 )
417 .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
418 this.mode = match this.mode {
419 NewSessionMode::Attach => NewSessionMode::Launch,
420 NewSessionMode::Launch => NewSessionMode::Attach,
421 _ => {
422 return;
423 }
424 };
425
426 this.mode_focus_handle(cx).focus(window);
427 }))
428 .child(
429 h_flex()
430 .w_full()
431 .justify_around()
432 .p_2()
433 .child(
434 h_flex()
435 .justify_start()
436 .w_full()
437 .child(
438 ToggleButton::new("debugger-session-ui-picker-button", "Launch")
439 .size(ButtonSize::Default)
440 .style(ui::ButtonStyle::Subtle)
441 .toggle_state(matches!(self.mode, NewSessionMode::Launch))
442 .on_click(cx.listener(|this, _, window, cx| {
443 this.mode = NewSessionMode::Launch;
444 this.mode_focus_handle(cx).focus(window);
445 cx.notify();
446 }))
447 .first(),
448 )
449 .child(
450 ToggleButton::new("debugger-session-ui-attach-button", "Attach")
451 .size(ButtonSize::Default)
452 .toggle_state(matches!(self.mode, NewSessionMode::Attach))
453 .style(ui::ButtonStyle::Subtle)
454 .on_click(cx.listener(|this, _, window, cx| {
455 this.mode = NewSessionMode::Attach;
456
457 if let Some(debugger) = this.debugger.as_ref() {
458 Self::update_attach_picker(
459 &this.attach_mode,
460 &debugger,
461 window,
462 cx,
463 );
464 }
465 this.mode_focus_handle(cx).focus(window);
466 cx.notify();
467 }))
468 .last(),
469 ),
470 )
471 .justify_between()
472 .border_color(cx.theme().colors().border_variant)
473 .border_b_1(),
474 )
475 .child(v_flex().child(self.render_mode(window, cx)))
476 .child(
477 h_flex()
478 .justify_between()
479 .gap_2()
480 .p_2()
481 .border_color(cx.theme().colors().border_variant)
482 .border_t_1()
483 .w_full()
484 .child(match self.mode {
485 NewSessionMode::Attach => {
486 div().child(self.adapter_drop_down_menu(window, cx))
487 }
488 NewSessionMode::Launch => div().child(
489 Button::new("new-session-modal-custom", "Custom").on_click({
490 let this = cx.weak_entity();
491 move |_, window, cx| {
492 this.update(cx, |this, cx| {
493 this.mode = NewSessionMode::Custom;
494 this.mode_focus_handle(cx).focus(window);
495 })
496 .ok();
497 }
498 }),
499 ),
500 NewSessionMode::Custom => h_flex()
501 .child(
502 Button::new("new-session-modal-back", "Save to .zed/debug.json...")
503 .on_click(cx.listener(|this, _, window, cx| {
504 let Some(save_scenario) = this
505 .debugger
506 .as_ref()
507 .and_then(|debugger| this.debug_scenario(&debugger, cx))
508 .zip(this.task_contexts.worktree())
509 .and_then(|(scenario, worktree_id)| {
510 this.debug_panel
511 .update(cx, |panel, cx| {
512 panel.save_scenario(
513 &scenario,
514 worktree_id,
515 window,
516 cx,
517 )
518 })
519 .ok()
520 })
521 else {
522 return;
523 };
524
525 this.save_scenario_state = Some(SaveScenarioState::Saving);
526
527 cx.spawn(async move |this, cx| {
528 let res = save_scenario.await;
529
530 this.update(cx, |this, _| match res {
531 Ok(saved_file) => {
532 this.save_scenario_state =
533 Some(SaveScenarioState::Saved(saved_file))
534 }
535 Err(error) => {
536 this.save_scenario_state =
537 Some(SaveScenarioState::Failed(
538 error.to_string().into(),
539 ))
540 }
541 })
542 .ok();
543
544 cx.background_executor()
545 .timer(Duration::from_secs(2))
546 .await;
547 this.update(cx, |this, _| {
548 this.save_scenario_state.take()
549 })
550 .ok();
551 })
552 .detach();
553 }))
554 .disabled(
555 self.debugger.is_none()
556 || self
557 .custom_mode
558 .read(cx)
559 .program
560 .read(cx)
561 .is_empty(cx)
562 || self.save_scenario_state.is_some(),
563 ),
564 )
565 .when_some(self.save_scenario_state.as_ref(), {
566 let this_entity = this.clone();
567
568 move |this, save_state| match save_state {
569 SaveScenarioState::Saved(saved_path) => this.child(
570 IconButton::new(
571 "new-session-modal-go-to-file",
572 IconName::ArrowUpRight,
573 )
574 .icon_size(IconSize::Small)
575 .icon_color(Color::Muted)
576 .on_click({
577 let this_entity = this_entity.clone();
578 let saved_path = saved_path.clone();
579 move |_, window, cx| {
580 window
581 .spawn(cx, {
582 let this_entity = this_entity.clone();
583 let saved_path = saved_path.clone();
584
585 async move |cx| {
586 this_entity
587 .update_in(
588 cx,
589 |this, window, cx| {
590 this.workspace.update(
591 cx,
592 |workspace, cx| {
593 workspace.open_path(
594 saved_path
595 .clone(),
596 None,
597 true,
598 window,
599 cx,
600 )
601 },
602 )
603 },
604 )??
605 .await?;
606
607 this_entity
608 .update(cx, |_, cx| {
609 cx.emit(DismissEvent)
610 })
611 .ok();
612
613 anyhow::Ok(())
614 }
615 })
616 .detach();
617 }
618 }),
619 ),
620 SaveScenarioState::Saving => this.child(
621 Icon::new(IconName::Spinner)
622 .size(IconSize::Small)
623 .color(Color::Muted)
624 .with_animation(
625 "Spinner",
626 Animation::new(Duration::from_secs(3)).repeat(),
627 |icon, delta| {
628 icon.transform(Transformation::rotate(
629 percentage(delta),
630 ))
631 },
632 ),
633 ),
634 SaveScenarioState::Failed(error_msg) => this.child(
635 IconButton::new("Failed Scenario Saved", IconName::X)
636 .icon_size(IconSize::Small)
637 .icon_color(Color::Error)
638 .tooltip(ui::Tooltip::text(error_msg.clone())),
639 ),
640 }
641 }),
642 })
643 .child(
644 Button::new("debugger-spawn", "Start")
645 .on_click(cx.listener(|this, _, window, cx| match &this.mode {
646 NewSessionMode::Launch => {
647 this.launch_picker.update(cx, |picker, cx| {
648 picker.delegate.confirm(true, window, cx)
649 })
650 }
651 _ => this.start_new_session(window, cx),
652 }))
653 .disabled(match self.mode {
654 NewSessionMode::Launch => {
655 !self.launch_picker.read(cx).delegate.matches.is_empty()
656 }
657 NewSessionMode::Attach => {
658 self.debugger.is_none()
659 || self
660 .attach_mode
661 .read(cx)
662 .attach_picker
663 .read(cx)
664 .picker
665 .read(cx)
666 .delegate
667 .match_count()
668 == 0
669 }
670 NewSessionMode::Custom => {
671 self.debugger.is_none()
672 || self.custom_mode.read(cx).program.read(cx).is_empty(cx)
673 }
674 }),
675 ),
676 )
677 }
678}
679
680impl EventEmitter<DismissEvent> for NewSessionModal {}
681impl Focusable for NewSessionModal {
682 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
683 self.mode_focus_handle(cx)
684 }
685}
686
687impl ModalView for NewSessionModal {}
688
689impl RenderOnce for AttachMode {
690 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
691 v_flex()
692 .w_full()
693 .track_focus(&self.attach_picker.focus_handle(cx))
694 .child(self.attach_picker.clone())
695 }
696}
697
698#[derive(Clone)]
699pub(super) struct CustomMode {
700 program: Entity<Editor>,
701 cwd: Entity<Editor>,
702 stop_on_entry: ToggleState,
703}
704
705impl CustomMode {
706 pub(super) fn new(
707 past_launch_config: Option<LaunchRequest>,
708 window: &mut Window,
709 cx: &mut App,
710 ) -> Entity<Self> {
711 let (past_program, past_cwd) = past_launch_config
712 .map(|config| (Some(config.program), config.cwd))
713 .unwrap_or_else(|| (None, None));
714
715 let program = cx.new(|cx| Editor::single_line(window, cx));
716 program.update(cx, |this, cx| {
717 this.set_placeholder_text("Run", cx);
718
719 if let Some(past_program) = past_program {
720 this.set_text(past_program, window, cx);
721 };
722 });
723 let cwd = cx.new(|cx| Editor::single_line(window, cx));
724 cwd.update(cx, |this, cx| {
725 this.set_placeholder_text("Working Directory", cx);
726 if let Some(past_cwd) = past_cwd {
727 this.set_text(past_cwd.to_string_lossy(), window, cx);
728 };
729 });
730 cx.new(|_| Self {
731 program,
732 cwd,
733 stop_on_entry: ToggleState::Unselected,
734 })
735 }
736
737 pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
738 let path = self.cwd.read(cx).text(cx);
739 if cfg!(windows) {
740 return task::LaunchRequest {
741 program: self.program.read(cx).text(cx),
742 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
743 args: Default::default(),
744 env: Default::default(),
745 };
746 }
747 let command = self.program.read(cx).text(cx);
748 let mut args = shlex::split(&command).into_iter().flatten().peekable();
749 let mut env = FxHashMap::default();
750 while args.peek().is_some_and(|arg| arg.contains('=')) {
751 let arg = args.next().unwrap();
752 let (lhs, rhs) = arg.split_once('=').unwrap();
753 env.insert(lhs.to_string(), rhs.to_string());
754 }
755
756 let program = if let Some(program) = args.next() {
757 program
758 } else {
759 env = FxHashMap::default();
760 command
761 };
762
763 let args = args.collect::<Vec<_>>();
764
765 task::LaunchRequest {
766 program,
767 cwd: path.is_empty().not().then(|| PathBuf::from(path)),
768 args,
769 env,
770 }
771 }
772
773 fn render(
774 &mut self,
775 adapter_menu: DropdownMenu,
776 window: &mut Window,
777 cx: &mut ui::Context<Self>,
778 ) -> impl IntoElement {
779 v_flex()
780 .p_2()
781 .w_full()
782 .gap_3()
783 .track_focus(&self.program.focus_handle(cx))
784 .child(
785 div().child(
786 Label::new("Program")
787 .size(ui::LabelSize::Small)
788 .color(Color::Muted),
789 ),
790 )
791 .child(render_editor(&self.program, window, cx))
792 .child(
793 h_flex()
794 .child(
795 Label::new("Debugger")
796 .size(ui::LabelSize::Small)
797 .color(Color::Muted),
798 )
799 .gap(ui::DynamicSpacing::Base08.rems(cx))
800 .child(adapter_menu),
801 )
802 .child(
803 CheckboxWithLabel::new(
804 "debugger-stop-on-entry",
805 Label::new("Stop on Entry").size(ui::LabelSize::Small),
806 self.stop_on_entry,
807 {
808 let this = cx.weak_entity();
809 move |state, _, cx| {
810 this.update(cx, |this, _| {
811 this.stop_on_entry = *state;
812 })
813 .ok();
814 }
815 },
816 )
817 .checkbox_position(ui::IconPosition::End),
818 )
819 }
820}
821
822#[derive(Clone)]
823pub(super) struct AttachMode {
824 pub(super) definition: DebugTaskDefinition,
825 pub(super) attach_picker: Entity<AttachModal>,
826}
827
828impl AttachMode {
829 pub(super) fn new(
830 debugger: Option<DebugAdapterName>,
831 workspace: WeakEntity<Workspace>,
832 window: &mut Window,
833 cx: &mut Context<NewSessionModal>,
834 ) -> Entity<Self> {
835 let definition = DebugTaskDefinition {
836 adapter: debugger.unwrap_or(DebugAdapterName("".into())),
837 label: "Attach New Session Setup".into(),
838 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
839 initialize_args: None,
840 tcp_connection: None,
841 stop_on_entry: Some(false),
842 };
843 let attach_picker = cx.new(|cx| {
844 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
845 window.focus(&modal.focus_handle(cx));
846
847 modal
848 });
849
850 cx.new(|_| Self {
851 definition,
852 attach_picker,
853 })
854 }
855 pub(super) fn debug_request(&self) -> task::AttachRequest {
856 task::AttachRequest { process_id: None }
857 }
858}
859
860pub(super) struct DebugScenarioDelegate {
861 task_store: Entity<TaskStore>,
862 candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
863 selected_index: usize,
864 matches: Vec<StringMatch>,
865 prompt: String,
866 debug_panel: WeakEntity<DebugPanel>,
867 workspace: WeakEntity<Workspace>,
868 task_contexts: Arc<TaskContexts>,
869}
870
871impl DebugScenarioDelegate {
872 pub(super) fn new(
873 debug_panel: WeakEntity<DebugPanel>,
874 workspace: WeakEntity<Workspace>,
875 task_store: Entity<TaskStore>,
876 task_contexts: Arc<TaskContexts>,
877 ) -> Self {
878 Self {
879 task_store,
880 candidates: None,
881 selected_index: 0,
882 matches: Vec::new(),
883 prompt: String::new(),
884 debug_panel,
885 workspace,
886 task_contexts,
887 }
888 }
889}
890
891impl PickerDelegate for DebugScenarioDelegate {
892 type ListItem = ui::ListItem;
893
894 fn match_count(&self) -> usize {
895 self.matches.len()
896 }
897
898 fn selected_index(&self) -> usize {
899 self.selected_index
900 }
901
902 fn set_selected_index(
903 &mut self,
904 ix: usize,
905 _window: &mut Window,
906 _cx: &mut Context<picker::Picker<Self>>,
907 ) {
908 self.selected_index = ix;
909 }
910
911 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
912 "".into()
913 }
914
915 fn update_matches(
916 &mut self,
917 query: String,
918 window: &mut Window,
919 cx: &mut Context<picker::Picker<Self>>,
920 ) -> gpui::Task<()> {
921 let candidates = self.candidates.clone();
922 let workspace = self.workspace.clone();
923 let task_store = self.task_store.clone();
924
925 cx.spawn_in(window, async move |picker, cx| {
926 let candidates: Vec<_> = match &candidates {
927 Some(candidates) => candidates
928 .into_iter()
929 .enumerate()
930 .map(|(index, (_, candidate))| {
931 StringMatchCandidate::new(index, candidate.label.as_ref())
932 })
933 .collect(),
934 None => {
935 let worktree_ids: Vec<_> = workspace
936 .update(cx, |this, cx| {
937 this.visible_worktrees(cx)
938 .map(|tree| tree.read(cx).id())
939 .collect()
940 })
941 .ok()
942 .unwrap_or_default();
943
944 let scenarios: Vec<_> = task_store
945 .update(cx, |task_store, cx| {
946 task_store.task_inventory().map(|item| {
947 item.read(cx).list_debug_scenarios(worktree_ids.into_iter())
948 })
949 })
950 .ok()
951 .flatten()
952 .unwrap_or_default();
953
954 picker
955 .update(cx, |picker, _| {
956 picker.delegate.candidates = Some(scenarios.clone());
957 })
958 .ok();
959
960 scenarios
961 .into_iter()
962 .enumerate()
963 .map(|(index, (_, candidate))| {
964 StringMatchCandidate::new(index, candidate.label.as_ref())
965 })
966 .collect()
967 }
968 };
969
970 let matches = fuzzy::match_strings(
971 &candidates,
972 &query,
973 true,
974 1000,
975 &Default::default(),
976 cx.background_executor().clone(),
977 )
978 .await;
979
980 picker
981 .update(cx, |picker, _| {
982 let delegate = &mut picker.delegate;
983
984 delegate.matches = matches;
985 delegate.prompt = query;
986
987 if delegate.matches.is_empty() {
988 delegate.selected_index = 0;
989 } else {
990 delegate.selected_index =
991 delegate.selected_index.min(delegate.matches.len() - 1);
992 }
993 })
994 .log_err();
995 })
996 }
997
998 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
999 let debug_scenario = self
1000 .matches
1001 .get(self.selected_index())
1002 .and_then(|match_candidate| {
1003 self.candidates
1004 .as_ref()
1005 .map(|candidates| candidates[match_candidate.candidate_id].clone())
1006 });
1007
1008 let Some((task_source_kind, debug_scenario)) = debug_scenario else {
1009 return;
1010 };
1011
1012 let (task_context, worktree_id) = if let TaskSourceKind::Worktree {
1013 id: worktree_id,
1014 directory_in_worktree: _,
1015 id_base: _,
1016 } = task_source_kind
1017 {
1018 self.task_contexts
1019 .task_context_for_worktree_id(worktree_id)
1020 .cloned()
1021 .map(|context| (context, Some(worktree_id)))
1022 } else {
1023 None
1024 }
1025 .unwrap_or_default();
1026
1027 self.debug_panel
1028 .update(cx, |panel, cx| {
1029 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1030 })
1031 .ok();
1032
1033 cx.emit(DismissEvent);
1034 }
1035
1036 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1037 cx.emit(DismissEvent);
1038 }
1039
1040 fn render_match(
1041 &self,
1042 ix: usize,
1043 selected: bool,
1044 window: &mut Window,
1045 cx: &mut Context<picker::Picker<Self>>,
1046 ) -> Option<Self::ListItem> {
1047 let hit = &self.matches[ix];
1048
1049 let highlighted_location = HighlightedMatch {
1050 text: hit.string.clone(),
1051 highlight_positions: hit.positions.clone(),
1052 char_count: hit.string.chars().count(),
1053 color: Color::Default,
1054 };
1055
1056 let icon = Icon::new(IconName::FileTree)
1057 .color(Color::Muted)
1058 .size(ui::IconSize::Small);
1059
1060 Some(
1061 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1062 .inset(true)
1063 .start_slot::<Icon>(icon)
1064 .spacing(ListItemSpacing::Sparse)
1065 .toggle_state(selected)
1066 .child(highlighted_location.render(window, cx)),
1067 )
1068 }
1069}