new_session_modal.rs

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