new_session_modal.rs

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