new_process_modal.rs

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