new_session_modal.rs

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