new_process_modal.rs

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