new_process_modal.rs

   1use anyhow::bail;
   2use collections::{FxHashMap, HashMap};
   3use language::LanguageRegistry;
   4use paths::local_debug_file_relative_path;
   5use std::{
   6    borrow::Cow,
   7    path::{Path, PathBuf},
   8    sync::Arc,
   9    time::Duration,
  10    usize,
  11};
  12use tasks_ui::{TaskOverrides, TasksModal};
  13
  14use dap::{
  15    DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
  16};
  17use editor::{Editor, EditorElement, EditorStyle};
  18use fuzzy::{StringMatch, StringMatchCandidate};
  19use gpui::{
  20    Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  21    HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
  22    Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
  23};
  24use itertools::Itertools as _;
  25use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
  26use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
  27use settings::{Settings, initial_local_debug_tasks_content};
  28use task::{DebugScenario, RevealTarget, ZedDebugConfig};
  29use theme::ThemeSettings;
  30use ui::{
  31    ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
  32    ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
  33    IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
  34    LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
  35    SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
  36    Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
  37};
  38use util::ResultExt;
  39use workspace::{ModalView, Workspace, pane};
  40
  41use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
  42
  43#[allow(unused)]
  44enum SaveScenarioState {
  45    Saving,
  46    Saved((ProjectPath, SharedString)),
  47    Failed(SharedString),
  48}
  49
  50pub(super) struct NewProcessModal {
  51    workspace: WeakEntity<Workspace>,
  52    debug_panel: WeakEntity<DebugPanel>,
  53    mode: NewProcessMode,
  54    debug_picker: Entity<Picker<DebugDelegate>>,
  55    attach_mode: Entity<AttachMode>,
  56    configure_mode: Entity<ConfigureMode>,
  57    task_mode: TaskMode,
  58    debugger: Option<DebugAdapterName>,
  59    save_scenario_state: Option<SaveScenarioState>,
  60    _subscriptions: [Subscription; 3],
  61}
  62
  63fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
  64    match request {
  65        DebugRequest::Launch(config) => {
  66            let last_path_component = Path::new(&config.program)
  67                .file_name()
  68                .map(|name| name.to_string_lossy())
  69                .unwrap_or_else(|| Cow::Borrowed(&config.program));
  70
  71            format!("{} ({debugger})", last_path_component).into()
  72        }
  73        DebugRequest::Attach(config) => format!(
  74            "pid: {} ({debugger})",
  75            config.process_id.unwrap_or(u32::MAX)
  76        )
  77        .into(),
  78    }
  79}
  80
  81impl NewProcessModal {
  82    pub(super) fn show(
  83        workspace: &mut Workspace,
  84        window: &mut Window,
  85        mode: NewProcessMode,
  86        reveal_target: Option<RevealTarget>,
  87        cx: &mut Context<Workspace>,
  88    ) {
  89        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
  90            return;
  91        };
  92        let task_store = workspace.project().read(cx).task_store().clone();
  93        let languages = workspace.app_state().languages.clone();
  94
  95        cx.spawn_in(window, async move |workspace, cx| {
  96            let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
  97                tasks_ui::task_contexts(workspace, window, cx)
  98            })?;
  99            workspace.update_in(cx, |workspace, window, cx| {
 100                let workspace_handle = workspace.weak_handle();
 101                workspace.toggle_modal(window, cx, |window, cx| {
 102                    let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
 103
 104                    let debug_picker = cx.new(|cx| {
 105                        let delegate =
 106                            DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
 107                        Picker::uniform_list(delegate, window, cx).modal(false)
 108                    });
 109
 110                    let configure_mode = ConfigureMode::new(window, cx);
 111
 112                    let task_overrides = Some(TaskOverrides { reveal_target });
 113
 114                    let task_mode = TaskMode {
 115                        task_modal: cx.new(|cx| {
 116                            TasksModal::new(
 117                                task_store.clone(),
 118                                Arc::new(TaskContexts::default()),
 119                                task_overrides,
 120                                false,
 121                                workspace_handle.clone(),
 122                                window,
 123                                cx,
 124                            )
 125                        }),
 126                    };
 127
 128                    let _subscriptions = [
 129                        cx.subscribe(&debug_picker, |_, _, _, cx| {
 130                            cx.emit(DismissEvent);
 131                        }),
 132                        cx.subscribe(
 133                            &attach_mode.read(cx).attach_picker.clone(),
 134                            |_, _, _, cx| {
 135                                cx.emit(DismissEvent);
 136                            },
 137                        ),
 138                        cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| {
 139                            cx.emit(DismissEvent)
 140                        }),
 141                    ];
 142
 143                    cx.spawn_in(window, {
 144                        let debug_picker = debug_picker.downgrade();
 145                        let configure_mode = configure_mode.downgrade();
 146                        let task_modal = task_mode.task_modal.downgrade();
 147                        let workspace = workspace_handle.clone();
 148
 149                        async move |this, cx| {
 150                            let task_contexts = task_contexts.await;
 151                            let task_contexts = Arc::new(task_contexts);
 152                            let lsp_task_sources = task_contexts.lsp_task_sources.clone();
 153                            let task_position = task_contexts.latest_selection;
 154                            // Get LSP tasks and filter out based on language vs lsp preference
 155                            let (lsp_tasks, prefer_lsp) =
 156                                workspace.update(cx, |workspace, cx| {
 157                                    let lsp_tasks = editor::lsp_tasks(
 158                                        workspace.project().clone(),
 159                                        &lsp_task_sources,
 160                                        task_position,
 161                                        cx,
 162                                    );
 163                                    let prefer_lsp = workspace
 164                                        .active_item(cx)
 165                                        .and_then(|item| item.downcast::<Editor>())
 166                                        .map(|editor| {
 167                                            editor
 168                                                .read(cx)
 169                                                .buffer()
 170                                                .read(cx)
 171                                                .language_settings(cx)
 172                                                .tasks
 173                                                .prefer_lsp
 174                                        })
 175                                        .unwrap_or(false);
 176                                    (lsp_tasks, prefer_lsp)
 177                                })?;
 178
 179                            let lsp_tasks = lsp_tasks.await;
 180                            let add_current_language_tasks = !prefer_lsp || lsp_tasks.is_empty();
 181
 182                            let lsp_tasks = lsp_tasks
 183                                .into_iter()
 184                                .flat_map(|(kind, tasks_with_locations)| {
 185                                    tasks_with_locations
 186                                        .into_iter()
 187                                        .sorted_by_key(|(location, task)| {
 188                                            (location.is_none(), task.resolved_label.clone())
 189                                        })
 190                                        .map(move |(_, task)| (kind.clone(), task))
 191                                })
 192                                .collect::<Vec<_>>();
 193
 194                            let Some(task_inventory) = task_store
 195                                .update(cx, |task_store, _| task_store.task_inventory().cloned())?
 196                            else {
 197                                return Ok(());
 198                            };
 199
 200                            let (used_tasks, current_resolved_tasks) = task_inventory
 201                                .update(cx, |task_inventory, cx| {
 202                                    task_inventory
 203                                        .used_and_current_resolved_tasks(task_contexts.clone(), cx)
 204                                })?
 205                                .await;
 206
 207                            if let Ok(task) = debug_picker.update(cx, |picker, cx| {
 208                                picker.delegate.tasks_loaded(
 209                                    task_contexts.clone(),
 210                                    languages,
 211                                    lsp_tasks.clone(),
 212                                    current_resolved_tasks.clone(),
 213                                    add_current_language_tasks,
 214                                    cx,
 215                                )
 216                            }) {
 217                                task.await;
 218                                debug_picker
 219                                    .update_in(cx, |picker, window, cx| {
 220                                        picker.refresh(window, cx);
 221                                        cx.notify();
 222                                    })
 223                                    .ok();
 224                            }
 225
 226                            if let Some(active_cwd) = task_contexts
 227                                .active_context()
 228                                .and_then(|context| context.cwd.clone())
 229                            {
 230                                configure_mode
 231                                    .update_in(cx, |configure_mode, window, cx| {
 232                                        configure_mode.load(active_cwd, window, cx);
 233                                    })
 234                                    .ok();
 235                            }
 236
 237                            task_modal
 238                                .update_in(cx, |task_modal, window, cx| {
 239                                    task_modal.tasks_loaded(
 240                                        task_contexts,
 241                                        lsp_tasks,
 242                                        used_tasks,
 243                                        current_resolved_tasks,
 244                                        add_current_language_tasks,
 245                                        window,
 246                                        cx,
 247                                    );
 248                                })
 249                                .ok();
 250
 251                            this.update(cx, |_, cx| {
 252                                cx.notify();
 253                            })
 254                            .ok();
 255
 256                            anyhow::Ok(())
 257                        }
 258                    })
 259                    .detach();
 260
 261                    Self {
 262                        debug_picker,
 263                        attach_mode,
 264                        configure_mode,
 265                        task_mode,
 266                        debugger: None,
 267                        mode,
 268                        debug_panel: debug_panel.downgrade(),
 269                        workspace: workspace_handle,
 270                        save_scenario_state: None,
 271                        _subscriptions,
 272                    }
 273                });
 274            })?;
 275
 276            anyhow::Ok(())
 277        })
 278        .detach();
 279    }
 280
 281    fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
 282        let dap_menu = self.adapter_drop_down_menu(window, cx);
 283        match self.mode {
 284            NewProcessMode::Task => self
 285                .task_mode
 286                .task_modal
 287                .read(cx)
 288                .picker
 289                .clone()
 290                .into_any_element(),
 291            NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
 292                this.clone().render(window, cx).into_any_element()
 293            }),
 294            NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
 295                this.clone().render(dap_menu, window, cx).into_any_element()
 296            }),
 297            NewProcessMode::Debug => v_flex()
 298                .w(rems(34.))
 299                .child(self.debug_picker.clone())
 300                .into_any_element(),
 301        }
 302    }
 303
 304    fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
 305        match self.mode {
 306            NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
 307            NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
 308            NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
 309            NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
 310        }
 311    }
 312
 313    fn debug_scenario(&self, debugger: &str, cx: &App) -> Task<Option<DebugScenario>> {
 314        let request = match self.mode {
 315            NewProcessMode::Launch => {
 316                DebugRequest::Launch(self.configure_mode.read(cx).debug_request(cx))
 317            }
 318            NewProcessMode::Attach => {
 319                DebugRequest::Attach(self.attach_mode.read(cx).debug_request())
 320            }
 321            _ => return Task::ready(None),
 322        };
 323        let label = suggested_label(&request, debugger);
 324
 325        let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
 326            Some(self.configure_mode.read(cx).stop_on_entry.selected())
 327        } else {
 328            None
 329        };
 330
 331        let session_scenario = ZedDebugConfig {
 332            adapter: debugger.to_owned().into(),
 333            label,
 334            request,
 335            stop_on_entry,
 336        };
 337
 338        let adapter = cx
 339            .global::<DapRegistry>()
 340            .adapter(&session_scenario.adapter);
 341
 342        cx.spawn(async move |_| adapter?.config_from_zed_format(session_scenario).await.ok())
 343    }
 344
 345    fn start_new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 346        if self.debugger.as_ref().is_none() {
 347            return;
 348        }
 349
 350        if let NewProcessMode::Debug = &self.mode {
 351            self.debug_picker.update(cx, |picker, cx| {
 352                picker.delegate.confirm(false, window, cx);
 353            });
 354            return;
 355        }
 356
 357        if let NewProcessMode::Launch = &self.mode {
 358            if self.configure_mode.read(cx).save_to_debug_json.selected() {
 359                self.save_debug_scenario(window, cx);
 360            }
 361        }
 362
 363        let Some(debugger) = self.debugger.clone() else {
 364            return;
 365        };
 366
 367        let debug_panel = self.debug_panel.clone();
 368        let Some(task_contexts) = self.task_contexts(cx) else {
 369            return;
 370        };
 371
 372        let task_context = task_contexts.active_context().cloned().unwrap_or_default();
 373        let worktree_id = task_contexts.worktree();
 374        let mode = self.mode;
 375        cx.spawn_in(window, async move |this, cx| {
 376            let Some(config) = this
 377                .update(cx, |this, cx| this.debug_scenario(&debugger, cx))?
 378                .await
 379            else {
 380                bail!("debug config not found in mode: {mode}");
 381            };
 382
 383            debug_panel.update_in(cx, |debug_panel, window, cx| {
 384                send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
 385                debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
 386            })?;
 387            this.update(cx, |_, cx| {
 388                cx.emit(DismissEvent);
 389            })
 390            .ok();
 391            anyhow::Ok(())
 392        })
 393        .detach_and_log_err(cx);
 394    }
 395
 396    fn update_attach_picker(
 397        attach: &Entity<AttachMode>,
 398        adapter: &DebugAdapterName,
 399        window: &mut Window,
 400        cx: &mut App,
 401    ) {
 402        attach.update(cx, |this, cx| {
 403            if adapter.0 != this.definition.adapter {
 404                this.definition.adapter = adapter.0.clone();
 405
 406                this.attach_picker.update(cx, |this, cx| {
 407                    this.picker.update(cx, |this, cx| {
 408                        this.delegate.definition.adapter = adapter.0.clone();
 409                        this.focus(window, cx);
 410                    })
 411                });
 412            }
 413
 414            cx.notify();
 415        })
 416    }
 417
 418    fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
 419        self.debug_picker.read(cx).delegate.task_contexts.clone()
 420    }
 421
 422    fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 423        let task_contents = self.task_contexts(cx);
 424        let Some(adapter) = self.debugger.as_ref() else {
 425            return;
 426        };
 427        let scenario = self.debug_scenario(&adapter, cx);
 428
 429        self.save_scenario_state = Some(SaveScenarioState::Saving);
 430
 431        cx.spawn_in(window, async move |this, cx| {
 432            let Some((scenario, worktree_id)) = scenario
 433                .await
 434                .zip(task_contents.and_then(|tcx| tcx.worktree()))
 435            else {
 436                this.update(cx, |this, _| {
 437                    this.save_scenario_state = Some(SaveScenarioState::Failed(
 438                        "Couldn't get scenario or task contents".into(),
 439                    ))
 440                })
 441                .ok();
 442                return;
 443            };
 444
 445            let Some(save_scenario) = this
 446                .update_in(cx, |this, window, cx| {
 447                    this.debug_panel
 448                        .update(cx, |panel, cx| {
 449                            panel.save_scenario(&scenario, worktree_id, window, cx)
 450                        })
 451                        .ok()
 452                })
 453                .ok()
 454                .flatten()
 455            else {
 456                return;
 457            };
 458            let res = save_scenario.await;
 459
 460            this.update(cx, |this, _| match res {
 461                Ok(saved_file) => {
 462                    this.save_scenario_state = Some(SaveScenarioState::Saved((
 463                        saved_file,
 464                        scenario.label.clone(),
 465                    )))
 466                }
 467                Err(error) => {
 468                    this.save_scenario_state =
 469                        Some(SaveScenarioState::Failed(error.to_string().into()))
 470                }
 471            })
 472            .ok();
 473
 474            cx.background_executor().timer(Duration::from_secs(3)).await;
 475            this.update(cx, |this, _| this.save_scenario_state.take())
 476                .ok();
 477        })
 478        .detach();
 479    }
 480
 481    fn adapter_drop_down_menu(
 482        &mut self,
 483        window: &mut Window,
 484        cx: &mut Context<Self>,
 485    ) -> ui::DropdownMenu {
 486        let workspace = self.workspace.clone();
 487        let weak = cx.weak_entity();
 488        let active_buffer = self.task_contexts(cx).and_then(|tc| {
 489            tc.active_item_context
 490                .as_ref()
 491                .and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
 492        });
 493
 494        let active_buffer_language = active_buffer
 495            .and_then(|buffer| buffer.read(cx).language())
 496            .cloned();
 497
 498        let mut available_adapters = workspace
 499            .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
 500            .unwrap_or_default();
 501        if let Some(language) = active_buffer_language {
 502            available_adapters.sort_by_key(|adapter| {
 503                language
 504                    .config()
 505                    .debuggers
 506                    .get_index_of(adapter.0.as_ref())
 507                    .unwrap_or(usize::MAX)
 508            });
 509            if self.debugger.is_none() {
 510                self.debugger = available_adapters.first().cloned();
 511            }
 512        }
 513
 514        let label = self
 515            .debugger
 516            .as_ref()
 517            .map(|d| d.0.clone())
 518            .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
 519
 520        DropdownMenu::new(
 521            "dap-adapter-picker",
 522            label,
 523            ContextMenu::build(window, cx, move |mut menu, _, _| {
 524                let setter_for_name = |name: DebugAdapterName| {
 525                    let weak = weak.clone();
 526                    move |window: &mut Window, cx: &mut App| {
 527                        weak.update(cx, |this, cx| {
 528                            this.debugger = Some(name.clone());
 529                            cx.notify();
 530                            if let NewProcessMode::Attach = &this.mode {
 531                                Self::update_attach_picker(&this.attach_mode, &name, window, cx);
 532                            }
 533                        })
 534                        .ok();
 535                    }
 536                };
 537
 538                for adapter in available_adapters.into_iter() {
 539                    menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
 540                }
 541
 542                menu
 543            }),
 544        )
 545    }
 546
 547    fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
 548        let this = cx.entity();
 549        window
 550            .spawn(cx, async move |cx| {
 551                let worktree_id = this.update(cx, |this, cx| {
 552                    let tcx = this.task_contexts(cx);
 553                    tcx?.worktree()
 554                })?;
 555
 556                let Some(worktree_id) = worktree_id else {
 557                    let _ = cx.prompt(
 558                        PromptLevel::Critical,
 559                        "Cannot open debug.json",
 560                        Some("You must have at least one project open"),
 561                        &[PromptButton::ok("Ok")],
 562                    );
 563                    return Ok(());
 564                };
 565
 566                let editor = this
 567                    .update_in(cx, |this, window, cx| {
 568                        this.workspace.update(cx, |workspace, cx| {
 569                            workspace.open_path(
 570                                ProjectPath {
 571                                    worktree_id,
 572                                    path: local_debug_file_relative_path().into(),
 573                                },
 574                                None,
 575                                true,
 576                                window,
 577                                cx,
 578                            )
 579                        })
 580                    })??
 581                    .await?;
 582
 583                cx.update(|_window, cx| {
 584                    if let Some(editor) = editor.act_as::<Editor>(cx) {
 585                        editor.update(cx, |editor, cx| {
 586                            editor.buffer().update(cx, |buffer, cx| {
 587                                if let Some(singleton) = buffer.as_singleton() {
 588                                    singleton.update(cx, |buffer, cx| {
 589                                        if buffer.is_empty() {
 590                                            buffer.edit(
 591                                                [(0..0, initial_local_debug_tasks_content())],
 592                                                None,
 593                                                cx,
 594                                            );
 595                                        }
 596                                    })
 597                                }
 598                            })
 599                        });
 600                    }
 601                })
 602                .ok();
 603
 604                this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
 605
 606                anyhow::Ok(())
 607            })
 608            .detach();
 609    }
 610}
 611
 612static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
 613
 614#[derive(Clone, Copy)]
 615pub(crate) enum NewProcessMode {
 616    Task,
 617    Launch,
 618    Attach,
 619    Debug,
 620}
 621
 622impl std::fmt::Display for NewProcessMode {
 623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 624        let mode = match self {
 625            NewProcessMode::Task => "Run",
 626            NewProcessMode::Debug => "Debug",
 627            NewProcessMode::Attach => "Attach",
 628            NewProcessMode::Launch => "Launch",
 629        };
 630
 631        write!(f, "{}", mode)
 632    }
 633}
 634
 635impl Focusable for NewProcessMode {
 636    fn focus_handle(&self, cx: &App) -> FocusHandle {
 637        cx.focus_handle()
 638    }
 639}
 640
 641fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
 642    let settings = ThemeSettings::get_global(cx);
 643    let theme = cx.theme();
 644
 645    let text_style = TextStyle {
 646        color: cx.theme().colors().text,
 647        font_family: settings.buffer_font.family.clone(),
 648        font_features: settings.buffer_font.features.clone(),
 649        font_size: settings.buffer_font_size(cx).into(),
 650        font_weight: settings.buffer_font.weight,
 651        line_height: relative(settings.buffer_line_height.value()),
 652        background_color: Some(theme.colors().editor_background),
 653        ..Default::default()
 654    };
 655
 656    let element = EditorElement::new(
 657        editor,
 658        EditorStyle {
 659            background: theme.colors().editor_background,
 660            local_player: theme.players().local(),
 661            text: text_style,
 662            ..Default::default()
 663        },
 664    );
 665
 666    div()
 667        .rounded_md()
 668        .p_1()
 669        .border_1()
 670        .border_color(theme.colors().border_variant)
 671        .when(
 672            editor.focus_handle(cx).contains_focused(window, cx),
 673            |this| this.border_color(theme.colors().border_focused),
 674        )
 675        .child(element)
 676        .bg(theme.colors().editor_background)
 677}
 678
 679impl Render for NewProcessModal {
 680    fn render(
 681        &mut self,
 682        window: &mut ui::Window,
 683        cx: &mut ui::Context<Self>,
 684    ) -> impl ui::IntoElement {
 685        v_flex()
 686            .size_full()
 687            .w(rems(34.))
 688            .key_context({
 689                let mut key_context = KeyContext::new_with_defaults();
 690                key_context.add("Pane");
 691                key_context.add("RunModal");
 692                key_context
 693            })
 694            .elevation_3(cx)
 695            .bg(cx.theme().colors().elevated_surface_background)
 696            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
 697                cx.emit(DismissEvent);
 698            }))
 699            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
 700                this.mode = match this.mode {
 701                    NewProcessMode::Task => NewProcessMode::Debug,
 702                    NewProcessMode::Debug => NewProcessMode::Attach,
 703                    NewProcessMode::Attach => NewProcessMode::Launch,
 704                    NewProcessMode::Launch => NewProcessMode::Task,
 705                };
 706
 707                this.mode_focus_handle(cx).focus(window);
 708            }))
 709            .on_action(
 710                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
 711                    this.mode = match this.mode {
 712                        NewProcessMode::Task => NewProcessMode::Launch,
 713                        NewProcessMode::Debug => NewProcessMode::Task,
 714                        NewProcessMode::Attach => NewProcessMode::Debug,
 715                        NewProcessMode::Launch => NewProcessMode::Attach,
 716                    };
 717
 718                    this.mode_focus_handle(cx).focus(window);
 719                }),
 720            )
 721            .child(
 722                h_flex()
 723                    .w_full()
 724                    .justify_around()
 725                    .p_2()
 726                    .child(
 727                        h_flex()
 728                            .justify_start()
 729                            .w_full()
 730                            .child(
 731                                ToggleButton::new(
 732                                    "debugger-session-ui-tasks-button",
 733                                    NewProcessMode::Task.to_string(),
 734                                )
 735                                .size(ButtonSize::Default)
 736                                .toggle_state(matches!(self.mode, NewProcessMode::Task))
 737                                .style(ui::ButtonStyle::Subtle)
 738                                .on_click(cx.listener(|this, _, window, cx| {
 739                                    this.mode = NewProcessMode::Task;
 740                                    this.mode_focus_handle(cx).focus(window);
 741                                    cx.notify();
 742                                }))
 743                                .tooltip(Tooltip::text("Run predefined task"))
 744                                .first(),
 745                            )
 746                            .child(
 747                                ToggleButton::new(
 748                                    "debugger-session-ui-launch-button",
 749                                    NewProcessMode::Debug.to_string(),
 750                                )
 751                                .size(ButtonSize::Default)
 752                                .style(ui::ButtonStyle::Subtle)
 753                                .toggle_state(matches!(self.mode, NewProcessMode::Debug))
 754                                .on_click(cx.listener(|this, _, window, cx| {
 755                                    this.mode = NewProcessMode::Debug;
 756                                    this.mode_focus_handle(cx).focus(window);
 757                                    cx.notify();
 758                                }))
 759                                .tooltip(Tooltip::text("Start a predefined debug scenario"))
 760                                .middle(),
 761                            )
 762                            .child(
 763                                ToggleButton::new(
 764                                    "debugger-session-ui-attach-button",
 765                                    NewProcessMode::Attach.to_string(),
 766                                )
 767                                .size(ButtonSize::Default)
 768                                .toggle_state(matches!(self.mode, NewProcessMode::Attach))
 769                                .style(ui::ButtonStyle::Subtle)
 770                                .on_click(cx.listener(|this, _, window, cx| {
 771                                    this.mode = NewProcessMode::Attach;
 772
 773                                    if let Some(debugger) = this.debugger.as_ref() {
 774                                        Self::update_attach_picker(
 775                                            &this.attach_mode,
 776                                            &debugger,
 777                                            window,
 778                                            cx,
 779                                        );
 780                                    }
 781                                    this.mode_focus_handle(cx).focus(window);
 782                                    cx.notify();
 783                                }))
 784                                .tooltip(Tooltip::text("Attach the debugger to a running process"))
 785                                .middle(),
 786                            )
 787                            .child(
 788                                ToggleButton::new(
 789                                    "debugger-session-ui-custom-button",
 790                                    NewProcessMode::Launch.to_string(),
 791                                )
 792                                .size(ButtonSize::Default)
 793                                .toggle_state(matches!(self.mode, NewProcessMode::Launch))
 794                                .style(ui::ButtonStyle::Subtle)
 795                                .on_click(cx.listener(|this, _, window, cx| {
 796                                    this.mode = NewProcessMode::Launch;
 797                                    this.mode_focus_handle(cx).focus(window);
 798                                    cx.notify();
 799                                }))
 800                                .tooltip(Tooltip::text("Launch a new process with a debugger"))
 801                                .last(),
 802                            ),
 803                    )
 804                    .justify_between()
 805                    .border_color(cx.theme().colors().border_variant)
 806                    .border_b_1(),
 807            )
 808            .child(v_flex().child(self.render_mode(window, cx)))
 809            .map(|el| {
 810                let container = h_flex()
 811                    .justify_between()
 812                    .gap_2()
 813                    .p_2()
 814                    .border_color(cx.theme().colors().border_variant)
 815                    .border_t_1()
 816                    .w_full();
 817                match self.mode {
 818                    NewProcessMode::Launch => el.child(
 819                        container
 820                            .child(
 821                                h_flex()
 822                                    .text_ui_sm(cx)
 823                                    .text_color(Color::Muted.color(cx))
 824                                    .child(
 825                                        InteractiveText::new(
 826                                            "open-debug-json",
 827                                            StyledText::new(
 828                                                "Open .zed/debug.json for advanced configuration",
 829                                            )
 830                                            .with_highlights([(
 831                                                5..20,
 832                                                HighlightStyle {
 833                                                    underline: Some(UnderlineStyle {
 834                                                        thickness: px(1.0),
 835                                                        color: None,
 836                                                        wavy: false,
 837                                                    }),
 838                                                    ..Default::default()
 839                                                },
 840                                            )]),
 841                                        )
 842                                        .on_click(
 843                                            vec![5..20],
 844                                            {
 845                                                let this = cx.entity();
 846                                                move |_, window, cx| {
 847                                                    this.update(cx, |this, cx| {
 848                                                        this.open_debug_json(window, cx);
 849                                                    })
 850                                                }
 851                                            },
 852                                        ),
 853                                    ),
 854                            )
 855                            .child(
 856                                Button::new("debugger-spawn", "Start")
 857                                    .on_click(cx.listener(|this, _, window, cx| {
 858                                        this.start_new_session(window, cx)
 859                                    }))
 860                                    .disabled(
 861                                        self.debugger.is_none()
 862                                            || self
 863                                                .configure_mode
 864                                                .read(cx)
 865                                                .program
 866                                                .read(cx)
 867                                                .is_empty(cx),
 868                                    ),
 869                            ),
 870                    ),
 871                    NewProcessMode::Attach => el.child(
 872                        container
 873                            .child(div().child(self.adapter_drop_down_menu(window, cx)))
 874                            .child(
 875                                Button::new("debugger-spawn", "Start")
 876                                    .on_click(cx.listener(|this, _, window, cx| {
 877                                        this.start_new_session(window, cx)
 878                                    }))
 879                                    .disabled(
 880                                        self.debugger.is_none()
 881                                            || self
 882                                                .attach_mode
 883                                                .read(cx)
 884                                                .attach_picker
 885                                                .read(cx)
 886                                                .picker
 887                                                .read(cx)
 888                                                .delegate
 889                                                .match_count()
 890                                                == 0,
 891                                    ),
 892                            ),
 893                    ),
 894                    NewProcessMode::Debug => el,
 895                    NewProcessMode::Task => el,
 896                }
 897            })
 898    }
 899}
 900
 901impl EventEmitter<DismissEvent> for NewProcessModal {}
 902impl Focusable for NewProcessModal {
 903    fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
 904        self.mode_focus_handle(cx)
 905    }
 906}
 907
 908impl ModalView for NewProcessModal {}
 909
 910impl RenderOnce for AttachMode {
 911    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
 912        v_flex()
 913            .w_full()
 914            .track_focus(&self.attach_picker.focus_handle(cx))
 915            .child(self.attach_picker.clone())
 916    }
 917}
 918
 919#[derive(Clone)]
 920pub(super) struct ConfigureMode {
 921    program: Entity<Editor>,
 922    cwd: Entity<Editor>,
 923    stop_on_entry: ToggleState,
 924    save_to_debug_json: ToggleState,
 925}
 926
 927impl ConfigureMode {
 928    pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
 929        let program = cx.new(|cx| Editor::single_line(window, cx));
 930        program.update(cx, |this, cx| {
 931            this.set_placeholder_text("ENV=Zed ~/bin/program --option", cx);
 932        });
 933
 934        let cwd = cx.new(|cx| Editor::single_line(window, cx));
 935        cwd.update(cx, |this, cx| {
 936            this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", cx);
 937        });
 938
 939        cx.new(|_| Self {
 940            program,
 941            cwd,
 942            stop_on_entry: ToggleState::Unselected,
 943            save_to_debug_json: ToggleState::Unselected,
 944        })
 945    }
 946
 947    fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
 948        self.cwd.update(cx, |editor, cx| {
 949            if editor.is_empty(cx) {
 950                editor.set_text(cwd.to_string_lossy(), window, cx);
 951            }
 952        });
 953    }
 954
 955    pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
 956        let cwd_text = self.cwd.read(cx).text(cx);
 957        let cwd = if cwd_text.is_empty() {
 958            None
 959        } else {
 960            Some(PathBuf::from(cwd_text))
 961        };
 962
 963        if cfg!(windows) {
 964            return task::LaunchRequest {
 965                program: self.program.read(cx).text(cx),
 966                cwd,
 967                args: Default::default(),
 968                env: Default::default(),
 969            };
 970        }
 971        let command = self.program.read(cx).text(cx);
 972        let mut args = shlex::split(&command).into_iter().flatten().peekable();
 973        let mut env = FxHashMap::default();
 974        while args.peek().is_some_and(|arg| arg.contains('=')) {
 975            let arg = args.next().unwrap();
 976            let (lhs, rhs) = arg.split_once('=').unwrap();
 977            env.insert(lhs.to_string(), rhs.to_string());
 978        }
 979
 980        let program = if let Some(program) = args.next() {
 981            program
 982        } else {
 983            env = FxHashMap::default();
 984            command
 985        };
 986
 987        let args = args.collect::<Vec<_>>();
 988
 989        task::LaunchRequest {
 990            program,
 991            cwd,
 992            args,
 993            env,
 994        }
 995    }
 996
 997    fn render(
 998        &mut self,
 999        adapter_menu: DropdownMenu,
1000        window: &mut Window,
1001        cx: &mut ui::Context<Self>,
1002    ) -> impl IntoElement {
1003        v_flex()
1004            .p_2()
1005            .w_full()
1006            .gap_3()
1007            .track_focus(&self.program.focus_handle(cx))
1008            .child(
1009                h_flex()
1010                    .child(
1011                        Label::new("Debugger")
1012                            .size(ui::LabelSize::Small)
1013                            .color(Color::Muted),
1014                    )
1015                    .gap(ui::DynamicSpacing::Base08.rems(cx))
1016                    .child(adapter_menu),
1017            )
1018            .child(
1019                Label::new("Program")
1020                    .size(ui::LabelSize::Small)
1021                    .color(Color::Muted),
1022            )
1023            .child(render_editor(&self.program, window, cx))
1024            .child(
1025                Label::new("Working Directory")
1026                    .size(ui::LabelSize::Small)
1027                    .color(Color::Muted),
1028            )
1029            .child(render_editor(&self.cwd, window, cx))
1030            .child(
1031                CheckboxWithLabel::new(
1032                    "debugger-stop-on-entry",
1033                    Label::new("Stop on Entry")
1034                        .size(ui::LabelSize::Small)
1035                        .color(Color::Muted),
1036                    self.stop_on_entry,
1037                    {
1038                        let this = cx.weak_entity();
1039                        move |state, _, cx| {
1040                            this.update(cx, |this, _| {
1041                                this.stop_on_entry = *state;
1042                            })
1043                            .ok();
1044                        }
1045                    },
1046                )
1047                .checkbox_position(ui::IconPosition::End),
1048            )
1049            .child(
1050                CheckboxWithLabel::new(
1051                    "debugger-save-to-debug-json",
1052                    Label::new("Save to debug.json")
1053                        .size(ui::LabelSize::Small)
1054                        .color(Color::Muted),
1055                    self.save_to_debug_json,
1056                    {
1057                        let this = cx.weak_entity();
1058                        move |state, _, cx| {
1059                            this.update(cx, |this, _| {
1060                                this.save_to_debug_json = *state;
1061                            })
1062                            .ok();
1063                        }
1064                    },
1065                )
1066                .checkbox_position(ui::IconPosition::End),
1067            )
1068    }
1069}
1070
1071#[derive(Clone)]
1072pub(super) struct AttachMode {
1073    pub(super) definition: ZedDebugConfig,
1074    pub(super) attach_picker: Entity<AttachModal>,
1075}
1076
1077impl AttachMode {
1078    pub(super) fn new(
1079        debugger: Option<DebugAdapterName>,
1080        workspace: WeakEntity<Workspace>,
1081        window: &mut Window,
1082        cx: &mut Context<NewProcessModal>,
1083    ) -> Entity<Self> {
1084        let definition = ZedDebugConfig {
1085            adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
1086            label: "Attach New Session Setup".into(),
1087            request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
1088            stop_on_entry: Some(false),
1089        };
1090        let attach_picker = cx.new(|cx| {
1091            let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
1092            window.focus(&modal.focus_handle(cx));
1093
1094            modal
1095        });
1096
1097        cx.new(|_| Self {
1098            definition,
1099            attach_picker,
1100        })
1101    }
1102    pub(super) fn debug_request(&self) -> task::AttachRequest {
1103        task::AttachRequest { process_id: None }
1104    }
1105}
1106
1107#[derive(Clone)]
1108pub(super) struct TaskMode {
1109    pub(super) task_modal: Entity<TasksModal>,
1110}
1111
1112pub(super) struct DebugDelegate {
1113    task_store: Entity<TaskStore>,
1114    candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
1115    selected_index: usize,
1116    matches: Vec<StringMatch>,
1117    prompt: String,
1118    debug_panel: WeakEntity<DebugPanel>,
1119    task_contexts: Option<Arc<TaskContexts>>,
1120    divider_index: Option<usize>,
1121    last_used_candidate_index: Option<usize>,
1122}
1123
1124impl DebugDelegate {
1125    pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
1126        Self {
1127            task_store,
1128            candidates: Vec::default(),
1129            selected_index: 0,
1130            matches: Vec::new(),
1131            prompt: String::new(),
1132            debug_panel,
1133            task_contexts: None,
1134            divider_index: None,
1135            last_used_candidate_index: None,
1136        }
1137    }
1138
1139    fn get_scenario_kind(
1140        languages: &Arc<LanguageRegistry>,
1141        dap_registry: &DapRegistry,
1142        scenario: DebugScenario,
1143    ) -> (Option<TaskSourceKind>, DebugScenario) {
1144        let language_names = languages.language_names();
1145        let language = dap_registry
1146            .adapter_language(&scenario.adapter)
1147            .map(|language| TaskSourceKind::Language {
1148                name: language.into(),
1149            });
1150
1151        let language = language.or_else(|| {
1152            scenario.label.split_whitespace().find_map(|word| {
1153                language_names
1154                    .iter()
1155                    .find(|name| name.eq_ignore_ascii_case(word))
1156                    .map(|name| TaskSourceKind::Language {
1157                        name: name.to_owned().into(),
1158                    })
1159            })
1160        });
1161
1162        (language, scenario)
1163    }
1164
1165    pub fn tasks_loaded(
1166        &mut self,
1167        task_contexts: Arc<TaskContexts>,
1168        languages: Arc<LanguageRegistry>,
1169        lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
1170        current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
1171        add_current_language_tasks: bool,
1172        cx: &mut Context<Picker<Self>>,
1173    ) -> Task<()> {
1174        self.task_contexts = Some(task_contexts.clone());
1175        let task = self.task_store.update(cx, |task_store, cx| {
1176            task_store.task_inventory().map(|inventory| {
1177                inventory.update(cx, |inventory, cx| {
1178                    inventory.list_debug_scenarios(
1179                        &task_contexts,
1180                        lsp_tasks,
1181                        current_resolved_tasks,
1182                        add_current_language_tasks,
1183                        cx,
1184                    )
1185                })
1186            })
1187        });
1188        cx.spawn(async move |this, cx| {
1189            let (recent, scenarios) = if let Some(task) = task {
1190                task.await
1191            } else {
1192                (Vec::new(), Vec::new())
1193            };
1194
1195            this.update(cx, |this, cx| {
1196                if !recent.is_empty() {
1197                    this.delegate.last_used_candidate_index = Some(recent.len() - 1);
1198                }
1199
1200                let dap_registry = cx.global::<DapRegistry>();
1201                let hide_vscode = scenarios.iter().any(|(kind, _)| match kind {
1202                    TaskSourceKind::Worktree {
1203                        id: _,
1204                        directory_in_worktree: dir,
1205                        id_base: _,
1206                    } => dir.ends_with(".zed"),
1207                    _ => false,
1208                });
1209
1210                this.delegate.candidates = recent
1211                    .into_iter()
1212                    .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1213                    .chain(
1214                        scenarios
1215                            .into_iter()
1216                            .filter(|(kind, _)| match kind {
1217                                TaskSourceKind::Worktree {
1218                                    id: _,
1219                                    directory_in_worktree: dir,
1220                                    id_base: _,
1221                                } => !(hide_vscode && dir.ends_with(".vscode")),
1222                                _ => true,
1223                            })
1224                            .map(|(kind, scenario)| {
1225                                let (language, scenario) =
1226                                    Self::get_scenario_kind(&languages, &dap_registry, scenario);
1227                                (language.or(Some(kind)), scenario)
1228                            }),
1229                    )
1230                    .collect();
1231            })
1232            .ok();
1233        })
1234    }
1235}
1236
1237impl PickerDelegate for DebugDelegate {
1238    type ListItem = ui::ListItem;
1239
1240    fn match_count(&self) -> usize {
1241        self.matches.len()
1242    }
1243
1244    fn selected_index(&self) -> usize {
1245        self.selected_index
1246    }
1247
1248    fn set_selected_index(
1249        &mut self,
1250        ix: usize,
1251        _window: &mut Window,
1252        _cx: &mut Context<picker::Picker<Self>>,
1253    ) {
1254        self.selected_index = ix;
1255    }
1256
1257    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1258        "Find a debug task, or debug a command.".into()
1259    }
1260
1261    fn update_matches(
1262        &mut self,
1263        query: String,
1264        window: &mut Window,
1265        cx: &mut Context<picker::Picker<Self>>,
1266    ) -> gpui::Task<()> {
1267        let candidates = self.candidates.clone();
1268
1269        cx.spawn_in(window, async move |picker, cx| {
1270            let candidates: Vec<_> = candidates
1271                .into_iter()
1272                .enumerate()
1273                .map(|(index, (_, candidate))| {
1274                    StringMatchCandidate::new(index, candidate.label.as_ref())
1275                })
1276                .collect();
1277
1278            let matches = fuzzy::match_strings(
1279                &candidates,
1280                &query,
1281                true,
1282                true,
1283                1000,
1284                &Default::default(),
1285                cx.background_executor().clone(),
1286            )
1287            .await;
1288
1289            picker
1290                .update(cx, |picker, _| {
1291                    let delegate = &mut picker.delegate;
1292
1293                    delegate.matches = matches;
1294                    delegate.prompt = query;
1295
1296                    delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1297                        let index = delegate
1298                            .matches
1299                            .partition_point(|matching_task| matching_task.candidate_id <= index);
1300                        Some(index).and_then(|index| (index != 0).then(|| index - 1))
1301                    });
1302
1303                    if delegate.matches.is_empty() {
1304                        delegate.selected_index = 0;
1305                    } else {
1306                        delegate.selected_index =
1307                            delegate.selected_index.min(delegate.matches.len() - 1);
1308                    }
1309                })
1310                .log_err();
1311        })
1312    }
1313
1314    fn separators_after_indices(&self) -> Vec<usize> {
1315        if let Some(i) = self.divider_index {
1316            vec![i]
1317        } else {
1318            Vec::new()
1319        }
1320    }
1321
1322    fn confirm_input(
1323        &mut self,
1324        _secondary: bool,
1325        window: &mut Window,
1326        cx: &mut Context<Picker<Self>>,
1327    ) {
1328        let text = self.prompt.clone();
1329        let (task_context, worktree_id) = self
1330            .task_contexts
1331            .as_ref()
1332            .and_then(|task_contexts| {
1333                Some((
1334                    task_contexts.active_context().cloned()?,
1335                    task_contexts.worktree(),
1336                ))
1337            })
1338            .unwrap_or_default();
1339
1340        let mut args = shlex::split(&text).into_iter().flatten().peekable();
1341        let mut env = HashMap::default();
1342        while args.peek().is_some_and(|arg| arg.contains('=')) {
1343            let arg = args.next().unwrap();
1344            let (lhs, rhs) = arg.split_once('=').unwrap();
1345            env.insert(lhs.to_string(), rhs.to_string());
1346        }
1347
1348        let program = if let Some(program) = args.next() {
1349            program
1350        } else {
1351            env = HashMap::default();
1352            text
1353        };
1354
1355        let args = args.collect::<Vec<_>>();
1356        let task = task::TaskTemplate {
1357            label: "one-off".to_owned(),
1358            env,
1359            command: program,
1360            args,
1361            ..Default::default()
1362        };
1363
1364        let Some(location) = self
1365            .task_contexts
1366            .as_ref()
1367            .and_then(|cx| cx.location().cloned())
1368        else {
1369            return;
1370        };
1371        let file = location.buffer.read(cx).file();
1372        let language = location.buffer.read(cx).language();
1373        let language_name = language.as_ref().map(|l| l.name());
1374        let Some(adapter): Option<DebugAdapterName> =
1375            language::language_settings::language_settings(language_name, file, cx)
1376                .debuggers
1377                .first()
1378                .map(SharedString::from)
1379                .map(Into::into)
1380                .or_else(|| {
1381                    language.and_then(|l| {
1382                        l.config()
1383                            .debuggers
1384                            .first()
1385                            .map(SharedString::from)
1386                            .map(Into::into)
1387                    })
1388                })
1389        else {
1390            return;
1391        };
1392        let locators = cx.global::<DapRegistry>().locators();
1393        cx.spawn_in(window, async move |this, cx| {
1394            let Some(debug_scenario) = cx
1395                .background_spawn(async move {
1396                    for locator in locators {
1397                        if let Some(scenario) =
1398                            locator.1.create_scenario(&task, "one-off", &adapter).await
1399                        {
1400                            return Some(scenario);
1401                        }
1402                    }
1403                    None
1404                })
1405                .await
1406            else {
1407                return;
1408            };
1409
1410            this.update_in(cx, |this, window, cx| {
1411                send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1412                this.delegate
1413                    .debug_panel
1414                    .update(cx, |panel, cx| {
1415                        panel.start_session(
1416                            debug_scenario,
1417                            task_context,
1418                            None,
1419                            worktree_id,
1420                            window,
1421                            cx,
1422                        );
1423                    })
1424                    .ok();
1425                cx.emit(DismissEvent);
1426            })
1427            .ok();
1428        })
1429        .detach();
1430    }
1431
1432    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1433        let debug_scenario = self
1434            .matches
1435            .get(self.selected_index())
1436            .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1437
1438        let Some((_, debug_scenario)) = debug_scenario else {
1439            return;
1440        };
1441
1442        let (task_context, worktree_id) = self
1443            .task_contexts
1444            .as_ref()
1445            .and_then(|task_contexts| {
1446                Some((
1447                    task_contexts.active_context().cloned()?,
1448                    task_contexts.worktree(),
1449                ))
1450            })
1451            .unwrap_or_default();
1452
1453        send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1454        self.debug_panel
1455            .update(cx, |panel, cx| {
1456                panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1457            })
1458            .ok();
1459
1460        cx.emit(DismissEvent);
1461    }
1462
1463    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1464        cx.emit(DismissEvent);
1465    }
1466
1467    fn render_footer(
1468        &self,
1469        window: &mut Window,
1470        cx: &mut Context<Picker<Self>>,
1471    ) -> Option<ui::AnyElement> {
1472        let current_modifiers = window.modifiers();
1473        let footer = h_flex()
1474            .w_full()
1475            .h_8()
1476            .p_2()
1477            .justify_between()
1478            .rounded_b_sm()
1479            .bg(cx.theme().colors().ghost_element_selected)
1480            .border_t_1()
1481            .border_color(cx.theme().colors().border_variant)
1482            .child(
1483                // TODO: add button to open selected task in debug.json
1484                h_flex().into_any_element(),
1485            )
1486            .map(|this| {
1487                if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
1488                    let action = picker::ConfirmInput {
1489                        secondary: current_modifiers.secondary(),
1490                    }
1491                    .boxed_clone();
1492                    this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
1493                        Button::new("launch-custom", "Launch Custom")
1494                            .label_size(LabelSize::Small)
1495                            .key_binding(keybind)
1496                            .on_click(move |_, window, cx| {
1497                                window.dispatch_action(action.boxed_clone(), cx)
1498                            })
1499                    }))
1500                } else {
1501                    this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
1502                        |keybind| {
1503                            let is_recent_selected =
1504                                self.divider_index >= Some(self.selected_index);
1505                            let run_entry_label =
1506                                if is_recent_selected { "Rerun" } else { "Spawn" };
1507
1508                            Button::new("spawn", run_entry_label)
1509                                .label_size(LabelSize::Small)
1510                                .key_binding(keybind)
1511                                .on_click(|_, window, cx| {
1512                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx);
1513                                })
1514                        },
1515                    ))
1516                }
1517            });
1518        Some(footer.into_any_element())
1519    }
1520
1521    fn render_match(
1522        &self,
1523        ix: usize,
1524        selected: bool,
1525        window: &mut Window,
1526        cx: &mut Context<picker::Picker<Self>>,
1527    ) -> Option<Self::ListItem> {
1528        let hit = &self.matches[ix];
1529
1530        let highlighted_location = HighlightedMatch {
1531            text: hit.string.clone(),
1532            highlight_positions: hit.positions.clone(),
1533            char_count: hit.string.chars().count(),
1534            color: Color::Default,
1535        };
1536        let task_kind = &self.candidates[hit.candidate_id].0;
1537
1538        let icon = match task_kind {
1539            Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1540            Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1541            Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1542            Some(TaskSourceKind::Lsp {
1543                language_name: name,
1544                ..
1545            })
1546            | Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1547                .get_icon_for_type(&name.to_lowercase(), cx)
1548                .map(Icon::from_path),
1549            None => Some(Icon::new(IconName::HistoryRerun)),
1550        }
1551        .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
1552        let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
1553            Some(Indicator::icon(
1554                Icon::new(IconName::BoltFilled)
1555                    .color(Color::Muted)
1556                    .size(IconSize::Small),
1557            ))
1558        } else {
1559            None
1560        };
1561        let icon = icon.map(|icon| {
1562            IconWithIndicator::new(icon, indicator)
1563                .indicator_border_color(Some(cx.theme().colors().border_transparent))
1564        });
1565
1566        Some(
1567            ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1568                .inset(true)
1569                .start_slot::<IconWithIndicator>(icon)
1570                .spacing(ListItemSpacing::Sparse)
1571                .toggle_state(selected)
1572                .child(highlighted_location.render(window, cx)),
1573        )
1574    }
1575}
1576
1577pub(crate) fn resolve_path(path: &mut String) {
1578    if path.starts_with('~') {
1579        let home = paths::home_dir().to_string_lossy().to_string();
1580        let trimmed_path = path.trim().to_owned();
1581        *path = trimmed_path.replacen('~', &home, 1);
1582    } else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
1583        *path = format!(
1584            "$ZED_WORKTREE_ROOT{}{}",
1585            std::path::MAIN_SEPARATOR,
1586            &strip_path
1587        );
1588    };
1589}