use dap::DebugRequest;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<String>,} pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec<StringMatch>, placeholder_text: Arc<str>, workspace: WeakEntity<Workspace>, pub(crate) debug_config: task::DebugTaskDefinition, candidates: Arc<[Candidate]>,} impl AttachModalDelegate { fn new( workspace: Entity<Workspace>, debug_config: task::DebugTaskDefinition, candidates: Arc<[Candidate]>, ) -> Self { Self { workspace: workspace.downgrade(), debug_config, 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<Picker<AttachModalDelegate>>,} impl AttachModal { pub fn new( workspace: Entity<Workspace>, debug_config: task::DebugTaskDefinition, modal: bool, window: &mut Window, cx: &mut Context<Self>, ) -> 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::<Vec<_>>(), } }) .collect(); processes.sort_by_key(|k| k.name.clone()); let processes = processes.into_iter().collect(); Self::with_processes(workspace, debug_config, processes, modal, window, cx) } pub(super) fn with_processes( workspace: Entity<Workspace>, debug_config: task::DebugTaskDefinition, processes: Arc<[Candidate]>, modal: bool, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( AttachModalDelegate::new(workspace, debug_config, 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<Self>) -> impl ui::IntoElement { v_flex() .key_context("AttachModal") .track_focus(&self.focus_handle(cx)) .w(rems(34.)) .child(self.picker.clone()) }} impl EventEmitter<DismissEvent> 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<Picker<Self>>, ) { self.selected_index = ix; } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> { self.placeholder_text.clone() } fn update_matches( &mut self, query: String, _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> 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::<Vec<_>>(), &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<Picker<Self>>) { 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.debug_config.request { DebugRequest::Attach(config) => { config.process_id = Some(candidate.pid); } DebugRequest::Launch(_) => { debug_panic!("Debugger attach modal used on launch debug config"); return; } } let definition = self.debug_config.clone(); let panel = self .workspace .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx)) .ok() .flatten(); if let Some(panel) = panel { panel.update(cx, |panel, cx| { panel.start_session(definition, window, cx); }); } cx.emit(DismissEvent); } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) { self.selected_index = 0; cx.emit(DismissEvent); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { 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::<Vec<_>>() .join(" "), )) .child( Label::new(format!( "{} {}", candidate.name, candidate .command .clone() .into_iter() .skip(1) .collect::<Vec<_>>() .join(" ") )) .size(LabelSize::Small) .color(Color::Muted), ), ), ), ) }} #[cfg(any(test, feature = "test-support"))]pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> { modal.picker.update(cx, |picker, _| { picker .delegate .matches .iter() .map(|hit| hit.string.clone()) .collect::<Vec<_>>() })}
use dap::DebugRequest;
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<String>,
}
pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
workspace: WeakEntity<Workspace>,
pub(crate) debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
}
impl AttachModalDelegate {
fn new(
workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
workspace: workspace.downgrade(),
debug_config,
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<Picker<AttachModalDelegate>>,
}
impl AttachModal {
pub fn new(
workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Self::with_processes(workspace, debug_config, processes, modal, window, cx)
}
pub(super) fn with_processes(
workspace: Entity<Workspace>,
debug_config: task::DebugTaskDefinition,
processes: Arc<[Candidate]>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
AttachModalDelegate::new(workspace, debug_config, 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<Self>) -> impl ui::IntoElement {
v_flex()
.key_context("AttachModal")
.track_focus(&self.focus_handle(cx))
.w(rems(34.))
.child(self.picker.clone())
}
}
impl EventEmitter<DismissEvent> 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<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
self.placeholder_text.clone()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> 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::<Vec<_>>(),
&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<Picker<Self>>) {
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.debug_config.request {
DebugRequest::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequest::Launch(_) => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}
}
let definition = self.debug_config.clone();
let panel = self
.workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten();
if let Some(panel) = panel {
panel.update(cx, |panel, cx| {
panel.start_session(definition, window, cx);
});
}
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = 0;
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
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::<Vec<_>>()
.join(" "),
))
.child(
Label::new(format!(
"{} {}",
candidate.name,
candidate
.command
.clone()
.into_iter()
.skip(1)
.collect::<Vec<_>>()
.join(" ")
))
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
}
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.update(cx, |picker, _| {
picker
.delegate
.matches
.iter()
.map(|hit| hit.string.clone())
.collect::<Vec<_>>()
})
}