Parse env vars and args from debug launch editor (#30538)

Julia Ryan , Cole Miller , and Conrad Irwin created

Release Notes:

- debugger: allow setting env vars and arguments on the launch command.

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                                  |  1 
crates/dap_adapters/src/codelldb.rs         |  4 ++
crates/dap_adapters/src/gdb.rs              |  4 +++
crates/dap_adapters/src/go.rs               |  3 +
crates/dap_adapters/src/javascript.rs       |  3 ++
crates/dap_adapters/src/php.rs              |  1 
crates/dap_adapters/src/python.rs           |  3 ++
crates/dap_adapters/src/ruby.rs             |  8 -----
crates/debugger_ui/Cargo.toml               |  1 
crates/debugger_ui/src/new_session_modal.rs | 27 +++++++++++++++++++---
crates/task/src/debug_format.rs             | 11 +++++++++
11 files changed, 53 insertions(+), 13 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4183,6 +4183,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "shlex",
  "sysinfo",
  "task",
  "tasks_ui",

crates/dap_adapters/src/codelldb.rs 🔗

@@ -42,7 +42,9 @@ impl CodeLldbDebugAdapter {
                 if !launch.args.is_empty() {
                     map.insert("args".into(), launch.args.clone().into());
                 }
-
+                if !launch.env.is_empty() {
+                    map.insert("env".into(), launch.env_json());
+                }
                 if let Some(stop_on_entry) = config.stop_on_entry {
                     map.insert("stopOnEntry".into(), stop_on_entry.into());
                 }

crates/dap_adapters/src/gdb.rs 🔗

@@ -35,6 +35,10 @@ impl GdbDebugAdapter {
                     map.insert("args".into(), launch.args.clone().into());
                 }
 
+                if !launch.env.is_empty() {
+                    map.insert("env".into(), launch.env_json());
+                }
+
                 if let Some(stop_on_entry) = config.stop_on_entry {
                     map.insert(
                         "stopAtBeginningOfMainSubprogram".into(),

crates/dap_adapters/src/go.rs 🔗

@@ -19,7 +19,8 @@ impl GoDebugAdapter {
             dap::DebugRequest::Launch(launch_config) => json!({
                 "program": launch_config.program,
                 "cwd": launch_config.cwd,
-                "args": launch_config.args
+                "args": launch_config.args,
+                "env": launch_config.env_json()
             }),
         };
 

crates/dap_adapters/src/javascript.rs 🔗

@@ -36,6 +36,9 @@ impl JsDebugAdapter {
                 if !launch.args.is_empty() {
                     map.insert("args".into(), launch.args.clone().into());
                 }
+                if !launch.env.is_empty() {
+                    map.insert("env".into(), launch.env_json());
+                }
 
                 if let Some(stop_on_entry) = config.stop_on_entry {
                     map.insert("stopOnEntry".into(), stop_on_entry.into());

crates/dap_adapters/src/php.rs 🔗

@@ -29,6 +29,7 @@ impl PhpDebugAdapter {
                     "program": launch_config.program,
                     "cwd": launch_config.cwd,
                     "args": launch_config.args,
+                    "env": launch_config.env_json(),
                     "stopOnEntry": config.stop_on_entry.unwrap_or_default(),
                 }),
                 request: config.request.to_dap(),

crates/dap_adapters/src/python.rs 🔗

@@ -32,6 +32,9 @@ impl PythonDebugAdapter {
             DebugRequest::Launch(launch) => {
                 map.insert("program".into(), launch.program.clone().into());
                 map.insert("args".into(), launch.args.clone().into());
+                if !launch.env.is_empty() {
+                    map.insert("env".into(), launch.env_json());
+                }
 
                 if let Some(stop_on_entry) = config.stop_on_entry {
                     map.insert("stopOnEntry".into(), stop_on_entry.into());

crates/dap_adapters/src/ruby.rs 🔗

@@ -62,7 +62,7 @@ impl DebugAdapter for RubyDebugAdapter {
         let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
-        let DebugRequest::Launch(mut launch) = definition.request.clone() else {
+        let DebugRequest::Launch(launch) = definition.request.clone() else {
             anyhow::bail!("rdbg does not yet support attaching");
         };
 
@@ -71,12 +71,6 @@ impl DebugAdapter for RubyDebugAdapter {
             format!("--port={}", port),
             format!("--host={}", host),
         ];
-        if launch.args.is_empty() {
-            let program = launch.program.clone();
-            let mut split = program.split(" ");
-            launch.program = split.next().unwrap().to_string();
-            launch.args = split.map(|s| s.to_string()).collect();
-        }
         if delegate.which(launch.program.as_ref()).is_some() {
             arguments.push("--command".to_string())
         }

crates/debugger_ui/Cargo.toml 🔗

@@ -51,6 +51,7 @@ rpc.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+shlex.workspace = true
 sysinfo.workspace = true
 task.workspace = true
 tasks_ui.workspace = true

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -1,3 +1,4 @@
+use collections::FxHashMap;
 use std::{
     borrow::Cow,
     ops::Not,
@@ -595,7 +596,7 @@ impl CustomMode {
 
         let program = cx.new(|cx| Editor::single_line(window, cx));
         program.update(cx, |this, cx| {
-            this.set_placeholder_text("Program path", cx);
+            this.set_placeholder_text("Run", cx);
 
             if let Some(past_program) = past_program {
                 this.set_text(past_program, window, cx);
@@ -617,11 +618,29 @@ impl CustomMode {
 
     pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
         let path = self.cwd.read(cx).text(cx);
+        let command = self.program.read(cx).text(cx);
+        let mut args = shlex::split(&command).into_iter().flatten().peekable();
+        let mut env = FxHashMap::default();
+        while args.peek().is_some_and(|arg| arg.contains('=')) {
+            let arg = args.next().unwrap();
+            let (lhs, rhs) = arg.split_once('=').unwrap();
+            env.insert(lhs.to_string(), rhs.to_string());
+        }
+
+        let program = if let Some(program) = args.next() {
+            program
+        } else {
+            env = FxHashMap::default();
+            command
+        };
+
+        let args = args.collect::<Vec<_>>();
+
         task::LaunchRequest {
-            program: self.program.read(cx).text(cx),
+            program,
             cwd: path.is_empty().not().then(|| PathBuf::from(path)),
-            args: Default::default(),
-            env: Default::default(),
+            args,
+            env,
         }
     }
 

crates/task/src/debug_format.rs 🔗

@@ -93,6 +93,17 @@ pub struct LaunchRequest {
     pub env: FxHashMap<String, String>,
 }
 
+impl LaunchRequest {
+    pub fn env_json(&self) -> serde_json::Value {
+        serde_json::Value::Object(
+            self.env
+                .iter()
+                .map(|(k, v)| (k.clone(), v.to_owned().into()))
+                .collect::<serde_json::Map<String, serde_json::Value>>(),
+        )
+    }
+}
+
 /// Represents the type that will determine which request to call on the debug adapter
 #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 #[serde(rename_all = "lowercase", untagged)]