diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index e39a842f63590375898c9870c345574e1932a788..64146169f53cfe44c3bdcb59b93e78d0f9223abd 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -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, } +pub(crate) enum ModalIntent { + ResolveProcessId(Option>>), + AttachToProcess(ZedDebugConfig), +} + pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, placeholder_text: Arc, - pub(crate) definition: ZedDebugConfig, + pub(crate) intent: ModalIntent, workspace: WeakEntity, candidates: Arc<[Candidate]>, } @@ -35,13 +42,13 @@ pub(crate) struct AttachModalDelegate { impl AttachModalDelegate { fn new( workspace: WeakEntity, - 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, project: Entity, 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, - definition: ZedDebugConfig, 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, 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>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { 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::(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::(|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::(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(); }) - .ok(); - this.update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - }) - .detach(); + .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); } @@ -338,7 +362,7 @@ fn get_processes_for_project(project: &Entity, cx: &mut App) -> Task, cx: &mut App) -> Task) -> Vec { +#[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 diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 174b8759b418946e1426d6194351a8212888c1d6..c343110b47527adc0f8d4e3e097a5f769b80682c 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -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, @@ -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 diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 0e21ef1268412418c381fc14617a917f9529834d..b82f839edee82f884c1419d44a2344c39c8abd0d 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -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 = + LazyLock::new(|| task::VariableName::PickProcessId.template_value()); + pub struct RunningState { session: Entity, thread_id: Option, @@ -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::>(); + + 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)) { diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 80e2b73d5a100bbd21462f0ad80def1997e184de..4df3ebf5196dea266287041e51dd65363d5f685c 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -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::(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::(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::(cx).is_none(), + "Attach modal should be dismissed after selection" + ); + }) + .unwrap(); +} diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 280bf5ccdb91271d7ff738654d507573c9d667d4..92d59094190de9924327722bb659058569869269 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -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, + commands: HashMap, } impl EnvVariableReplacer { fn new(variables: HashMap) -> Self { - Self { variables } + Self { + variables, + commands: HashMap::default(), + } + } + + fn with_commands( + mut self, + commands: impl IntoIterator, + ) -> 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() { diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 9b3f2e808b750e60d494c0b92abf78bb8c698227..bef64c8d409ce0b3ee2c062ee97196af7ea22311 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -68,7 +68,11 @@ impl TryFrom 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 + }]) + ); + } }