new_session_modal.rs

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