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                    .children(self.adapter_drop_down_menu(window, cx))
 633                    .border_color(cx.theme().colors().border_variant)
 634                    .border_b_1(),
 635            )
 636            .child(v_flex().child(self.mode.clone().render(window, cx)))
 637            .child(
 638                h_flex()
 639                    .justify_between()
 640                    .gap_2()
 641                    .p_2()
 642                    .border_color(cx.theme().colors().border_variant)
 643                    .border_t_1()
 644                    .w_full()
 645                    .child(
 646                        matches!(self.mode, NewSessionMode::Scenario(_))
 647                            .not()
 648                            .then(|| {
 649                                self.debug_config_drop_down_menu(window, cx)
 650                                    .into_any_element()
 651                            })
 652                            .unwrap_or_else(|| v_flex().w_full().into_any_element()),
 653                    )
 654                    .child(
 655                        h_flex()
 656                            .justify_end()
 657                            .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
 658                                let weak = cx.weak_entity();
 659                                this.child(
 660                                    CheckboxWithLabel::new(
 661                                        "debugger-stop-on-entry",
 662                                        Label::new("Stop on Entry").size(ui::LabelSize::Small),
 663                                        self.stop_on_entry,
 664                                        move |state, _, cx| {
 665                                            weak.update(cx, |this, _| {
 666                                                this.stop_on_entry = *state;
 667                                            })
 668                                            .ok();
 669                                        },
 670                                    )
 671                                    .checkbox_position(ui::IconPosition::End),
 672                                )
 673                            })
 674                            .child(
 675                                Button::new("debugger-spawn", "Start")
 676                                    .on_click(cx.listener(|this, _, window, cx| match &this.mode {
 677                                        NewSessionMode::Scenario(picker) => {
 678                                            picker.update(cx, |picker, cx| {
 679                                                picker.delegate.confirm(true, window, cx)
 680                                            })
 681                                        }
 682                                        _ => this.start_new_session(window, cx),
 683                                    }))
 684                                    .disabled(match self.mode {
 685                                        NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
 686                                        NewSessionMode::Attach(_) => {
 687                                            self.debugger.is_none() || !self.mode.has_match(cx)
 688                                        }
 689                                        NewSessionMode::Launch(_) => self.debugger.is_none(),
 690                                    }),
 691                            ),
 692                    ),
 693            )
 694    }
 695}
 696
 697impl EventEmitter<DismissEvent> for NewSessionModal {}
 698impl Focusable for NewSessionModal {
 699    fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
 700        self.mode.focus_handle(cx)
 701    }
 702}
 703
 704impl ModalView for NewSessionModal {}
 705
 706impl RenderOnce for LaunchMode {
 707    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 708        v_flex()
 709            .p_2()
 710            .w_full()
 711            .gap_3()
 712            .track_focus(&self.program.focus_handle(cx))
 713            .child(
 714                div().child(
 715                    Label::new("Program")
 716                        .size(ui::LabelSize::Small)
 717                        .color(Color::Muted),
 718                ),
 719            )
 720            .child(render_editor(&self.program, window, cx))
 721            .child(
 722                div().child(
 723                    Label::new("Working Directory")
 724                        .size(ui::LabelSize::Small)
 725                        .color(Color::Muted),
 726                ),
 727            )
 728            .child(render_editor(&self.cwd, window, cx))
 729    }
 730}
 731
 732impl RenderOnce for AttachMode {
 733    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
 734        v_flex()
 735            .w_full()
 736            .track_focus(&self.attach_picker.focus_handle(cx))
 737            .child(self.attach_picker.clone())
 738    }
 739}
 740
 741use std::rc::Rc;
 742
 743#[derive(Clone)]
 744pub(super) struct LaunchMode {
 745    program: Entity<Editor>,
 746    cwd: Entity<Editor>,
 747}
 748
 749impl LaunchMode {
 750    pub(super) fn new(
 751        past_launch_config: Option<LaunchRequest>,
 752        window: &mut Window,
 753        cx: &mut App,
 754    ) -> Entity<Self> {
 755        let (past_program, past_cwd) = past_launch_config
 756            .map(|config| (Some(config.program), config.cwd))
 757            .unwrap_or_else(|| (None, None));
 758
 759        let program = cx.new(|cx| Editor::single_line(window, cx));
 760        program.update(cx, |this, cx| {
 761            this.set_placeholder_text("Program path", cx);
 762
 763            if let Some(past_program) = past_program {
 764                this.set_text(past_program, window, cx);
 765            };
 766        });
 767        let cwd = cx.new(|cx| Editor::single_line(window, cx));
 768        cwd.update(cx, |this, cx| {
 769            this.set_placeholder_text("Working Directory", cx);
 770            if let Some(past_cwd) = past_cwd {
 771                this.set_text(past_cwd.to_string_lossy(), window, cx);
 772            };
 773        });
 774        cx.new(|_| Self { program, cwd })
 775    }
 776
 777    pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
 778        let path = self.cwd.read(cx).text(cx);
 779        task::LaunchRequest {
 780            program: self.program.read(cx).text(cx),
 781            cwd: path.is_empty().not().then(|| PathBuf::from(path)),
 782            args: Default::default(),
 783            env: Default::default(),
 784        }
 785    }
 786}
 787
 788#[derive(Clone)]
 789pub(super) struct AttachMode {
 790    pub(super) definition: DebugTaskDefinition,
 791    pub(super) attach_picker: Entity<AttachModal>,
 792    _subscription: Rc<Subscription>,
 793}
 794
 795impl AttachMode {
 796    pub(super) fn new(
 797        debugger: Option<DebugAdapterName>,
 798        workspace: Entity<Workspace>,
 799        window: &mut Window,
 800        cx: &mut Context<NewSessionModal>,
 801    ) -> Entity<Self> {
 802        let definition = DebugTaskDefinition {
 803            adapter: debugger.unwrap_or(DebugAdapterName("".into())),
 804            label: "Attach New Session Setup".into(),
 805            request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
 806            initialize_args: None,
 807            tcp_connection: None,
 808            stop_on_entry: Some(false),
 809        };
 810        let attach_picker = cx.new(|cx| {
 811            let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
 812            window.focus(&modal.focus_handle(cx));
 813
 814            modal
 815        });
 816
 817        let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
 818            cx.emit(DismissEvent);
 819        });
 820
 821        cx.new(|_| Self {
 822            definition,
 823            attach_picker,
 824            _subscription: Rc::new(subscription),
 825        })
 826    }
 827    pub(super) fn debug_task(&self) -> task::AttachRequest {
 828        task::AttachRequest { process_id: None }
 829    }
 830}
 831
 832pub(super) struct DebugScenarioDelegate {
 833    task_store: Entity<TaskStore>,
 834    candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
 835    selected_index: usize,
 836    matches: Vec<StringMatch>,
 837    prompt: String,
 838    debug_panel: WeakEntity<DebugPanel>,
 839    workspace: WeakEntity<Workspace>,
 840}
 841
 842impl DebugScenarioDelegate {
 843    pub(super) fn new(
 844        debug_panel: WeakEntity<DebugPanel>,
 845        workspace: WeakEntity<Workspace>,
 846        task_store: Entity<TaskStore>,
 847    ) -> Self {
 848        Self {
 849            task_store,
 850            candidates: None,
 851            selected_index: 0,
 852            matches: Vec::new(),
 853            prompt: String::new(),
 854            debug_panel,
 855            workspace,
 856        }
 857    }
 858}
 859
 860impl PickerDelegate for DebugScenarioDelegate {
 861    type ListItem = ui::ListItem;
 862
 863    fn match_count(&self) -> usize {
 864        self.matches.len()
 865    }
 866
 867    fn selected_index(&self) -> usize {
 868        self.selected_index
 869    }
 870
 871    fn set_selected_index(
 872        &mut self,
 873        ix: usize,
 874        _window: &mut Window,
 875        _cx: &mut Context<picker::Picker<Self>>,
 876    ) {
 877        self.selected_index = ix;
 878    }
 879
 880    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
 881        "".into()
 882    }
 883
 884    fn update_matches(
 885        &mut self,
 886        query: String,
 887        window: &mut Window,
 888        cx: &mut Context<picker::Picker<Self>>,
 889    ) -> gpui::Task<()> {
 890        let candidates: Vec<_> = match &self.candidates {
 891            Some(candidates) => candidates
 892                .into_iter()
 893                .enumerate()
 894                .map(|(index, (_, candidate))| {
 895                    StringMatchCandidate::new(index, candidate.label.as_ref())
 896                })
 897                .collect(),
 898            None => {
 899                let worktree_ids: Vec<_> = self
 900                    .workspace
 901                    .update(cx, |this, cx| {
 902                        this.visible_worktrees(cx)
 903                            .map(|tree| tree.read(cx).id())
 904                            .collect()
 905                    })
 906                    .ok()
 907                    .unwrap_or_default();
 908
 909                let scenarios: Vec<_> = self
 910                    .task_store
 911                    .read(cx)
 912                    .task_inventory()
 913                    .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
 914                    .unwrap_or_default();
 915
 916                self.candidates = Some(scenarios.clone());
 917
 918                scenarios
 919                    .into_iter()
 920                    .enumerate()
 921                    .map(|(index, (_, candidate))| {
 922                        StringMatchCandidate::new(index, candidate.label.as_ref())
 923                    })
 924                    .collect()
 925            }
 926        };
 927
 928        cx.spawn_in(window, async move |picker, cx| {
 929            let matches = fuzzy::match_strings(
 930                &candidates,
 931                &query,
 932                true,
 933                1000,
 934                &Default::default(),
 935                cx.background_executor().clone(),
 936            )
 937            .await;
 938
 939            picker
 940                .update(cx, |picker, _| {
 941                    let delegate = &mut picker.delegate;
 942
 943                    delegate.matches = matches;
 944                    delegate.prompt = query;
 945
 946                    if delegate.matches.is_empty() {
 947                        delegate.selected_index = 0;
 948                    } else {
 949                        delegate.selected_index =
 950                            delegate.selected_index.min(delegate.matches.len() - 1);
 951                    }
 952                })
 953                .log_err();
 954        })
 955    }
 956
 957    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
 958        let debug_scenario = self
 959            .matches
 960            .get(self.selected_index())
 961            .and_then(|match_candidate| {
 962                self.candidates
 963                    .as_ref()
 964                    .map(|candidates| candidates[match_candidate.candidate_id].clone())
 965            });
 966
 967        let Some((task_source_kind, debug_scenario)) = debug_scenario else {
 968            return;
 969        };
 970
 971        let task_context = if let TaskSourceKind::Worktree {
 972            id: worktree_id,
 973            directory_in_worktree: _,
 974            id_base: _,
 975        } = task_source_kind
 976        {
 977            let workspace = self.workspace.clone();
 978
 979            cx.spawn_in(window, async move |_, cx| {
 980                workspace
 981                    .update_in(cx, |workspace, window, cx| {
 982                        tasks_ui::task_contexts(workspace, window, cx)
 983                    })
 984                    .ok()?
 985                    .await
 986                    .task_context_for_worktree_id(worktree_id)
 987                    .cloned()
 988                    .map(|context| (context, Some(worktree_id)))
 989            })
 990        } else {
 991            gpui::Task::ready(None)
 992        };
 993
 994        cx.spawn_in(window, async move |this, cx| {
 995            let (task_context, worktree_id) = task_context.await.unwrap_or_default();
 996
 997            this.update_in(cx, |this, window, cx| {
 998                this.delegate
 999                    .debug_panel
1000                    .update(cx, |panel, cx| {
1001                        panel.start_session(
1002                            debug_scenario,
1003                            task_context,
1004                            None,
1005                            worktree_id,
1006                            window,
1007                            cx,
1008                        );
1009                    })
1010                    .ok();
1011
1012                cx.emit(DismissEvent);
1013            })
1014            .ok();
1015        })
1016        .detach();
1017    }
1018
1019    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1020        cx.emit(DismissEvent);
1021    }
1022
1023    fn render_match(
1024        &self,
1025        ix: usize,
1026        selected: bool,
1027        window: &mut Window,
1028        cx: &mut Context<picker::Picker<Self>>,
1029    ) -> Option<Self::ListItem> {
1030        let hit = &self.matches[ix];
1031
1032        let highlighted_location = HighlightedMatch {
1033            text: hit.string.clone(),
1034            highlight_positions: hit.positions.clone(),
1035            char_count: hit.string.chars().count(),
1036            color: Color::Default,
1037        };
1038
1039        let icon = Icon::new(IconName::FileTree)
1040            .color(Color::Muted)
1041            .size(ui::IconSize::Small);
1042
1043        Some(
1044            ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1045                .inset(true)
1046                .start_slot::<Icon>(icon)
1047                .spacing(ListItemSpacing::Sparse)
1048                .toggle_state(selected)
1049                .child(highlighted_location.render(window, cx)),
1050        )
1051    }
1052}