use dap::DebugRequest; use dap::adapters::DebugTaskDefinition; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use gpui::{Subscription, WeakEntity}; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use sysinfo::System; use ui::{Context, Tooltip, prelude::*}; use ui::{ListItem, ListItemSpacing}; use util::debug_panic; 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) struct AttachModalDelegate { selected_index: usize, matches: Vec, placeholder_text: Arc, pub(crate) definition: DebugTaskDefinition, workspace: WeakEntity, candidates: Arc<[Candidate]>, } impl AttachModalDelegate { fn new( workspace: Entity, definition: DebugTaskDefinition, candidates: Arc<[Candidate]>, ) -> Self { Self { workspace: workspace.downgrade(), definition, candidates, 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 fn new( definition: DebugTaskDefinition, workspace: Entity, modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { let mut processes: Box<[_]> = System::new_all() .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().to_string()) .collect::>(), } }) .collect(); processes.sort_by_key(|k| k.name.clone()); let processes = processes.into_iter().collect(); Self::with_processes(workspace, definition, processes, modal, window, cx) } pub(super) fn with_processes( workspace: Entity, definition: DebugTaskDefinition, processes: Arc<[Candidate]>, modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( AttachModalDelegate::new(workspace, definition, 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 .update(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, 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, _: 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) }); let Some(candidate) = candidate else { return cx.emit(DismissEvent); }; match &mut self.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 scenario = self.definition.to_scenario(); let panel = self .workspace .update(cx, |workspace, cx| workspace.panel::(cx)) .ok() .flatten(); if let Some(panel) = panel { panel.update(cx, |panel, cx| { panel.start_session(scenario, Default::default(), None, None, window, cx); }); } cx.emit(DismissEvent); } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.selected_index = 0; cx.emit(DismissEvent); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _: &mut Context>, ) -> Option { let hit = &self.matches[ix]; let candidate = self.candidates.get(hit.candidate_id)?; Some( ListItem::new(SharedString::from(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(SharedString::from(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), ), ), ), ) } } #[cfg(any(test, feature = "test-support"))] pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context) -> Vec { modal.picker.update(cx, |picker, _| { picker .delegate .matches .iter() .map(|hit| hit.string.clone()) .collect::>() }) }