use dap::{DapRegistry, DebugRequest}; use futures::channel::oneshot; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, TaskExt}; use gpui::{Subscription, WeakEntity}; use picker::{Picker, PickerDelegate}; use project::Project; use rpc::proto; use task::ZedDebugConfig; use util::debug_panic; use std::sync::Arc; use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; use ui::{Context, Tooltip, prelude::*}; use ui::{ListItem, ListItemSpacing}; use workspace::{ModalView, Workspace}; use crate::debugger_panel::DebugPanel; #[derive(Debug, Clone)] pub(super) struct Candidate { pub(super) pid: u32, pub(super) name: SharedString, pub(super) command: Vec, } pub(crate) enum ModalIntent { ResolveProcessId(Option>>), AttachToProcess(ZedDebugConfig), } pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, placeholder_text: Arc, pub(crate) intent: ModalIntent, workspace: WeakEntity, candidates: Arc<[Candidate]>, } impl AttachModalDelegate { fn new( workspace: WeakEntity, intent: ModalIntent, candidates: Arc<[Candidate]>, ) -> Self { Self { workspace, candidates, intent, selected_index: 0, matches: Vec::default(), placeholder_text: Arc::from("Select the process you want to attach the debugger to"), } } } pub struct AttachModal { _subscription: Subscription, pub(crate) picker: Entity>, } impl AttachModal { pub(crate) fn new( intent: ModalIntent, workspace: WeakEntity, project: Entity, modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { let processes_task = get_processes_for_project(&project, cx); let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx); cx.spawn_in(window, async move |this, cx| { let processes = processes_task.await; this.update_in(cx, |modal, window, cx| { modal.picker.update(cx, |picker, cx| { picker.delegate.candidates = processes; picker.refresh(window, cx); }); })?; anyhow::Ok(()) }) .detach_and_log_err(cx); modal } pub(super) fn with_processes( workspace: WeakEntity, processes: Arc<[Candidate]>, modal: bool, intent: ModalIntent, window: &mut Window, cx: &mut Context, ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( AttachModalDelegate::new(workspace, intent, processes), window, cx, ) .modal(modal) }); Self { _subscription: cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); }), picker, } } } impl Render for AttachModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { v_flex() .key_context("AttachModal") .track_focus(&self.focus_handle(cx)) .w(rems(34.)) .child(self.picker.clone()) } } impl EventEmitter for AttachModal {} impl Focusable for AttachModal { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.picker.read(cx).focus_handle(cx) } } impl ModalView for AttachModal {} impl PickerDelegate for AttachModalDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { self.matches.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _: &mut Context>, ) { self.selected_index = ix; } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc { self.placeholder_text.clone() } fn update_matches( &mut self, query: String, _window: &mut Window, cx: &mut Context>, ) -> gpui::Task<()> { cx.spawn(async move |this, cx| { let Some(processes) = this .read_with(cx, |this, _| this.delegate.candidates.clone()) .ok() else { return; }; let matches = fuzzy::match_strings( &processes .iter() .enumerate() .map(|(id, candidate)| { StringMatchCandidate::new( id, format!( "{} {} {}", candidate.command.join(" "), candidate.pid, candidate.name ) .as_str(), ) }) .collect::>(), &query, true, true, 100, &Default::default(), cx.background_executor().clone(), ) .await; this.update(cx, |this, _| { let delegate = &mut this.delegate; delegate.matches = matches; if delegate.matches.is_empty() { delegate.selected_index = 0; } else { delegate.selected_index = delegate.selected_index.min(delegate.matches.len() - 1); } }) .ok(); }) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let candidate = self .matches .get(self.selected_index()) .and_then(|current_match| { let ix = current_match.candidate_id; self.candidates.get(ix) }); match &mut self.intent { ModalIntent::ResolveProcessId(sender) => { cx.emit(DismissEvent); if let Some(sender) = sender.take() { sender .send(candidate.map(|candidate| candidate.pid as i32)) .ok(); } } ModalIntent::AttachToProcess(definition) => { let Some(candidate) = candidate else { return cx.emit(DismissEvent); }; match &mut definition.request { DebugRequest::Attach(config) => { config.process_id = Some(candidate.pid); } DebugRequest::Launch(_) => { debug_panic!("Debugger attach modal used on launch debug config"); return; } } let workspace = self.workspace.clone(); let Some(panel) = workspace .update(cx, |workspace, cx| workspace.panel::(cx)) .ok() .flatten() else { return; }; let Some(adapter) = cx.read_global::(|registry, _| { registry.adapter(&definition.adapter) }) else { return; }; let definition = definition.clone(); cx.spawn_in(window, async move |this, cx| { let Ok(scenario) = adapter.config_from_zed_format(definition).await else { return; }; panel .update_in(cx, |panel, window, cx| { panel.start_session( scenario, Default::default(), None, None, window, cx, ); }) .ok(); this.update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); }) .detach(); } } } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.selected_index = 0; match &mut self.intent { ModalIntent::ResolveProcessId(sender) => { if let Some(sender) = sender.take() { sender.send(None).ok(); } } ModalIntent::AttachToProcess(_) => {} } cx.emit(DismissEvent); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _: &mut Context>, ) -> Option { let hit = &self.matches.get(ix)?; let candidate = self.candidates.get(hit.candidate_id)?; Some( ListItem::new(format!("process-entry-{ix}")) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( v_flex() .items_start() .child(Label::new(format!("{} {}", candidate.name, candidate.pid))) .child( div() .id(format!("process-entry-{ix}-command")) .tooltip(Tooltip::text( candidate .command .clone() .into_iter() .collect::>() .join(" "), )) .child( Label::new(format!( "{} {}", candidate.name, candidate .command .clone() .into_iter() .skip(1) .collect::>() .join(" ") )) .size(LabelSize::Small) .color(Color::Muted), ), ), ), ) } } fn get_processes_for_project(project: &Entity, cx: &mut App) -> Task> { let project = project.read(cx); if let Some(remote_client) = project.remote_client() { let proto_client = remote_client.read(cx).proto_client(); cx.background_spawn(async move { let response = proto_client .request(proto::GetProcesses { project_id: proto::REMOTE_SERVER_PROJECT_ID, }) .await .unwrap_or_else(|_| proto::GetProcessesResponse { processes: Vec::new(), }); let mut processes: Vec = response .processes .into_iter() .map(|p| Candidate { pid: p.pid, name: p.name.into(), command: p.command, }) .collect(); processes.sort_by_key(|k| k.name.clone()); Arc::from(processes.into_boxed_slice()) }) } else { let refresh_kind = RefreshKind::nothing().with_processes( ProcessRefreshKind::nothing() .without_tasks() .with_cmd(UpdateKind::Always), ); let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind) .processes() .values() .map(|process| { let name = process.name().to_string_lossy().into_owned(); Candidate { name: name.into(), pid: process.pid().as_u32(), command: process .cmd() .iter() .map(|s| s.to_string_lossy().into_owned()) .collect::>(), } }) .collect(); processes.sort_by_key(|k| k.name.clone()); let processes = processes.into_iter().collect(); Task::ready(processes) } } #[cfg(test)] pub(crate) fn set_candidates( modal: &AttachModal, candidates: Arc<[Candidate]>, window: &mut Window, cx: &mut Context, ) { modal.picker.update(cx, |picker, cx| { picker.delegate.candidates = candidates; picker.refresh(window, cx); }); } #[cfg(test)] pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context) -> Vec { modal.picker.read_with(cx, |picker, _| { picker .delegate .matches .iter() .map(|hit| hit.string.clone()) .collect::>() }) }