Detailed changes
@@ -1,4 +1,5 @@
use dap::{DapRegistry, DebugRequest};
+use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
@@ -9,6 +10,7 @@ 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};
@@ -23,11 +25,16 @@ pub(super) struct Candidate {
pub(super) command: Vec<String>,
}
+pub(crate) enum ModalIntent {
+ ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
+ AttachToProcess(ZedDebugConfig),
+}
+
pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
- pub(crate) definition: ZedDebugConfig,
+ pub(crate) intent: ModalIntent,
workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>,
}
@@ -35,13 +42,13 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate {
fn new(
workspace: WeakEntity<Workspace>,
- definition: ZedDebugConfig,
+ intent: ModalIntent,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
workspace,
- definition,
candidates,
+ intent,
selected_index: 0,
matches: Vec::default(),
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
@@ -55,8 +62,8 @@ pub struct AttachModal {
}
impl AttachModal {
- pub fn new(
- definition: ZedDebugConfig,
+ pub(crate) fn new(
+ intent: ModalIntent,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
modal: bool,
@@ -65,7 +72,7 @@ impl AttachModal {
) -> Self {
let processes_task = get_processes_for_project(&project, cx);
- let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, 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;
@@ -84,15 +91,15 @@ impl AttachModal {
pub(super) fn with_processes(
workspace: WeakEntity<Workspace>,
- definition: ZedDebugConfig,
processes: Arc<[Candidate]>,
modal: bool,
+ intent: ModalIntent,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
- AttachModalDelegate::new(workspace, definition, processes),
+ AttachModalDelegate::new(workspace, intent, processes),
window,
cx,
)
@@ -207,7 +214,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
- fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -216,69 +223,86 @@ impl PickerDelegate for AttachModalDelegate {
self.candidates.get(ix)
});
- let Some(candidate) = candidate else {
- return cx.emit(DismissEvent);
- };
+ match &mut self.intent {
+ ModalIntent::ResolveProcessId(sender) => {
+ 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;
+ if let Some(sender) = sender.take() {
+ sender
+ .send(candidate.map(|candidate| candidate.pid as i32))
+ .ok();
+ }
}
- }
-
- let workspace = self.workspace.clone();
- let Some(panel) = workspace
- .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
- .ok()
- .flatten()
- else {
- return;
- };
-
- if secondary {
- // let Some(id) = worktree_id else { return };
- // cx.spawn_in(window, async move |_, cx| {
- // panel
- // .update_in(cx, |debug_panel, window, cx| {
- // debug_panel.save_scenario(&debug_scenario, id, window, cx)
- // })?
- // .await?;
- // anyhow::Ok(())
- // })
- // .detach_and_log_err(cx);
- }
- let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
- registry.adapter(&self.definition.adapter)
- }) else {
- return;
- };
-
- let definition = self.definition.clone();
- cx.spawn_in(window, async move |this, cx| {
- let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
- return;
- };
+ 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;
+ }
+ }
- panel
- .update_in(cx, |panel, window, cx| {
- panel.start_session(scenario, Default::default(), None, None, window, cx);
+ let workspace = self.workspace.clone();
+ let Some(panel) = workspace
+ .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+ .ok()
+ .flatten()
+ else {
+ return;
+ };
+
+ let Some(adapter) = cx.read_global::<DapRegistry, _>(|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();
})
- .ok();
- this.update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- })
- .detach();
+ .detach();
+ }
+ }
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
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);
}
@@ -338,7 +362,7 @@ fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Ar
if let Some(remote_client) = project.remote_client() {
let proto_client = remote_client.read(cx).proto_client();
- cx.spawn(async move |_cx| {
+ cx.background_spawn(async move {
let response = proto_client
.request(proto::GetProcesses {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
@@ -389,8 +413,21 @@ fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Ar
}
}
-#[cfg(any(test, feature = "test-support"))]
-pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
+#[cfg(test)]
+pub(crate) fn set_candidates(
+ modal: &AttachModal,
+ candidates: Arc<[Candidate]>,
+ window: &mut Window,
+ cx: &mut Context<AttachModal>,
+) {
+ 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<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {
picker
.delegate
@@ -29,10 +29,13 @@ use ui::{
KeyBinding, ListItem, ListItemSpacing, ToggleButtonGroup, ToggleButtonSimple, ToggleState,
Tooltip, prelude::*,
};
-use util::{ResultExt, rel_path::RelPath, shell::ShellKind};
+use util::{ResultExt, debug_panic, rel_path::RelPath, shell::ShellKind};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
-use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
+use crate::{
+ attach_modal::{AttachModal, ModalIntent},
+ debugger_panel::DebugPanel,
+};
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
@@ -395,8 +398,15 @@ impl NewProcessModal {
this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| {
- this.delegate.definition.adapter = adapter.0.clone();
- this.focus(window, cx);
+ match &mut this.delegate.intent {
+ ModalIntent::AttachToProcess(definition) => {
+ definition.adapter = adapter.0.clone();
+ this.focus(window, cx);
+ },
+ ModalIntent::ResolveProcessId(_) => {
+ debug_panic!("Attach picker attempted to update config when in resolve Process ID mode");
+ }
+ }
})
});
}
@@ -942,7 +952,14 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
- let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx);
+ let modal = AttachModal::new(
+ ModalIntent::AttachToProcess(definition.clone()),
+ workspace,
+ project,
+ false,
+ window,
+ cx,
+ );
window.focus(&modal.focus_handle(cx));
modal
@@ -5,16 +5,23 @@ pub(crate) mod memory_view;
pub(crate) mod module_list;
pub mod stack_frame_list;
pub mod variable_list;
-use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
+use std::{
+ any::Any,
+ ops::ControlFlow,
+ path::PathBuf,
+ sync::{Arc, LazyLock},
+ time::Duration,
+};
use crate::{
ToggleExpandItem,
+ attach_modal::{AttachModal, ModalIntent},
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
session::running::memory_view::MemoryView,
};
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Context as _, Result, anyhow, bail};
use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
use console::Console;
@@ -56,6 +63,9 @@ use workspace::{
Workspace, item::TabContentParams, move_item, pane::Event,
};
+static PROCESS_ID_PLACEHOLDER: LazyLock<String> =
+ LazyLock::new(|| task::VariableName::PickProcessId.template_value());
+
pub struct RunningState {
session: Entity<Session>,
thread_id: Option<ThreadId>,
@@ -653,6 +663,40 @@ impl RunningState {
}
}
+ pub(crate) fn contains_substring(config: &serde_json::Value, substring: &str) -> bool {
+ match config {
+ serde_json::Value::Object(obj) => obj
+ .values()
+ .any(|value| Self::contains_substring(value, substring)),
+ serde_json::Value::Array(array) => array
+ .iter()
+ .any(|value| Self::contains_substring(value, substring)),
+ serde_json::Value::String(s) => s.contains(substring),
+ _ => false,
+ }
+ }
+
+ pub(crate) fn substitute_process_id_in_config(config: &mut serde_json::Value, process_id: i32) {
+ match config {
+ serde_json::Value::Object(obj) => {
+ obj.values_mut().for_each(|value| {
+ Self::substitute_process_id_in_config(value, process_id);
+ });
+ }
+ serde_json::Value::Array(array) => {
+ array.iter_mut().for_each(|value| {
+ Self::substitute_process_id_in_config(value, process_id);
+ });
+ }
+ serde_json::Value::String(s) => {
+ if s.contains(PROCESS_ID_PLACEHOLDER.as_str()) {
+ *s = s.replace(PROCESS_ID_PLACEHOLDER.as_str(), &process_id.to_string());
+ }
+ }
+ _ => {}
+ }
+ }
+
pub(crate) fn relativize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
@@ -955,6 +999,31 @@ impl RunningState {
Self::relativize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
+ if Self::contains_substring(&config, PROCESS_ID_PLACEHOLDER.as_str()) || label.as_ref().contains(PROCESS_ID_PLACEHOLDER.as_str()) {
+ let (tx, rx) = futures::channel::oneshot::channel::<Option<i32>>();
+
+ let weak_workspace_clone = weak_workspace.clone();
+ weak_workspace.update_in(cx, |workspace, window, cx| {
+ let project = workspace.project().clone();
+ workspace.toggle_modal(window, cx, |window, cx| {
+ AttachModal::new(
+ ModalIntent::ResolveProcessId(Some(tx)),
+ weak_workspace_clone,
+ project,
+ true,
+ window,
+ cx,
+ )
+ });
+ }).ok();
+
+ let Some(process_id) = rx.await.ok().flatten() else {
+ bail!("No process selected with config that contains {}", PROCESS_ID_PLACEHOLDER.as_str())
+ };
+
+ Self::substitute_process_id_in_config(&mut config, process_id);
+ }
+
let request_type = match dap_registry
.adapter(&adapter)
.with_context(|| format!("{}: is not a valid adapter name", &adapter)) {
@@ -1,4 +1,8 @@
-use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
+use crate::{
+ attach_modal::{Candidate, ModalIntent},
+ tests::start_debug_session_with,
+ *,
+};
use attach_modal::AttachModal;
use dap::{FakeAdapter, adapters::DebugTaskDefinition};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
@@ -98,12 +102,6 @@ async fn test_show_attach_modal_and_select_process(
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
- task::ZedDebugConfig {
- adapter: FakeAdapter::ADAPTER_NAME.into(),
- request: dap::DebugRequest::Attach(AttachRequest::default()),
- label: "attach example".into(),
- stop_on_entry: None,
- },
vec![
Candidate {
pid: 0,
@@ -124,6 +122,12 @@ async fn test_show_attach_modal_and_select_process(
.into_iter()
.collect(),
true,
+ ModalIntent::AttachToProcess(task::ZedDebugConfig {
+ adapter: FakeAdapter::ADAPTER_NAME.into(),
+ request: dap::DebugRequest::Attach(AttachRequest::default()),
+ label: "attach example".into(),
+ stop_on_entry: None,
+ }),
window,
cx,
)
@@ -138,8 +142,7 @@ async fn test_show_attach_modal_and_select_process(
// assert we got the expected processes
workspace
.update(cx, |_, window, cx| {
- let names =
- attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
+ let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(3, names.len());
attach_modal.update(cx, |this, cx| {
@@ -153,8 +156,7 @@ async fn test_show_attach_modal_and_select_process(
// assert we got the expected processes
workspace
.update(cx, |_, _, cx| {
- let names =
- attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
+ let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(2, names.len());
})
@@ -171,3 +173,139 @@ async fn test_show_attach_modal_and_select_process(
})
.unwrap();
}
+
+#[gpui::test]
+async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "main.rs": "First line\nSecond line\nThird line\nFourth line",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ let _initialize_subscription =
+ project::debugger::test::intercept_debug_sessions(cx, |client| {
+ client.on_request::<dap::requests::Attach, _>(move |_, args| {
+ let raw = &args.raw;
+ assert_eq!(raw["request"], "attach");
+ assert_eq!(
+ raw["process_id"], "42",
+ "verify process id has been replaced"
+ );
+
+ Ok(())
+ });
+ });
+
+ let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
+ workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.start_debug_session(
+ DebugTaskDefinition {
+ adapter: FakeAdapter::ADAPTER_NAME.into(),
+ label: "attach with picker".into(),
+ config: json!({
+ "request": "attach",
+ "process_id": pick_pid_placeholder,
+ }),
+ tcp_connection: None,
+ }
+ .to_scenario(),
+ task::TaskContext::default(),
+ None,
+ None,
+ window,
+ cx,
+ )
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ let attach_modal = workspace
+ .update(cx, |workspace, _window, cx| {
+ workspace.active_modal::<AttachModal>(cx)
+ })
+ .unwrap();
+
+ assert!(
+ attach_modal.is_some(),
+ "Attach modal should open when config contains ZED_PICK_PID"
+ );
+
+ let attach_modal = attach_modal.unwrap();
+
+ workspace
+ .update(cx, |_, window, cx| {
+ attach_modal.update(cx, |modal, cx| {
+ attach_modal::set_candidates(
+ modal,
+ vec![
+ Candidate {
+ pid: 10,
+ name: "process-1".into(),
+ command: vec![],
+ },
+ Candidate {
+ pid: 42,
+ name: "target-process".into(),
+ command: vec![],
+ },
+ Candidate {
+ pid: 99,
+ name: "process-3".into(),
+ command: vec![],
+ },
+ ]
+ .into_iter()
+ .collect(),
+ window,
+ cx,
+ )
+ })
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ workspace
+ .update(cx, |_, window, cx| {
+ attach_modal.update(cx, |modal, cx| {
+ modal.picker.update(cx, |picker, cx| {
+ picker.set_query("target", window, cx);
+ })
+ })
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ workspace
+ .update(cx, |_, _, cx| {
+ let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
+ assert_eq!(names.len(), 1);
+ assert_eq!(names[0], " 42 target-process");
+ })
+ .unwrap();
+
+ cx.dispatch_action(Confirm);
+ cx.run_until_parked();
+
+ workspace
+ .update(cx, |workspace, _window, cx| {
+ assert!(
+ workspace.active_modal::<AttachModal>(cx).is_none(),
+ "Attach modal should be dismissed after selection"
+ );
+ })
+ .unwrap();
+}
@@ -173,6 +173,9 @@ pub enum VariableName {
SelectedText,
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
RunnableSymbol,
+ /// Open a Picker to select a process ID to use in place
+ /// Can only be used to debug configurations
+ PickProcessId,
/// Custom variable, provided by the plugin or other external source.
/// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables.
Custom(Cow<'static, str>),
@@ -240,6 +243,7 @@ impl std::fmt::Display for VariableName {
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
+ Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"),
Self::Custom(s) => write!(
f,
"{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}"
@@ -346,15 +350,28 @@ pub fn shell_to_proto(shell: Shell) -> proto::Shell {
}
type VsCodeEnvVariable = String;
+type VsCodeCommand = String;
type ZedEnvVariable = String;
struct EnvVariableReplacer {
variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>,
+ commands: HashMap<VsCodeCommand, ZedEnvVariable>,
}
impl EnvVariableReplacer {
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
- Self { variables }
+ Self {
+ variables,
+ commands: HashMap::default(),
+ }
+ }
+
+ fn with_commands(
+ mut self,
+ commands: impl IntoIterator<Item = (VsCodeCommand, ZedEnvVariable)>,
+ ) -> Self {
+ self.commands = commands.into_iter().collect();
+ self
}
fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
@@ -380,7 +397,13 @@ impl EnvVariableReplacer {
if left == "env" && !right.is_empty() {
let variable_name = &right[1..];
return Some(format!("${{{variable_name}}}"));
+ } else if left == "command" && !right.is_empty() {
+ let command_name = &right[1..];
+ if let Some(replacement_command) = self.commands.get(command_name) {
+ return Some(format!("${{{replacement_command}}}"));
+ }
}
+
let (variable_name, default) = (left, right);
let append_previous_default = |ret: &mut String| {
if !default.is_empty() {
@@ -68,7 +68,11 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
VariableName::RelativeFile.to_string(),
),
("file".to_owned(), VariableName::File.to_string()),
- ]));
+ ]))
+ .with_commands([(
+ "pickMyProcess".to_owned(),
+ VariableName::PickProcessId.to_string(),
+ )]);
let templates = file
.configurations
.into_iter()
@@ -96,7 +100,7 @@ fn task_type_to_adapter_name(task_type: &str) -> String {
mod tests {
use serde_json::json;
- use crate::{DebugScenario, DebugTaskFile};
+ use crate::{DebugScenario, DebugTaskFile, VariableName};
use super::VsCodeDebugTaskFile;
@@ -152,4 +156,39 @@ mod tests {
}])
);
}
+
+ #[test]
+ fn test_command_pickmyprocess_replacement() {
+ let raw = r#"
+ {
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Attach to Process",
+ "request": "attach",
+ "type": "cppdbg",
+ "processId": "${command:pickMyProcess}"
+ }
+ ]
+ }
+ "#;
+ let parsed: VsCodeDebugTaskFile =
+ serde_json_lenient::from_str(raw).expect("deserializing launch.json");
+ let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
+
+ let expected_placeholder = format!("${{{}}}", VariableName::PickProcessId);
+ pretty_assertions::assert_eq!(
+ zed,
+ DebugTaskFile(vec![DebugScenario {
+ label: "Attach to Process".into(),
+ adapter: "CodeLLDB".into(),
+ config: json!({
+ "request": "attach",
+ "processId": expected_placeholder,
+ }),
+ tcp_connection: None,
+ build: None
+ }])
+ );
+ }
}