new_session_modal.rs

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