new_session_modal.rs

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