new_process_modal.rs

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