new_session_modal.rs

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