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