From 01295aa687d726343e3b58ac4f0b3ae3b0d123eb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 6 Jul 2025 01:48:55 +0200 Subject: [PATCH] debugger: Fix the JavaScript debug terminal scenario (#33924) There were a couple of things preventing this from working: - our hack to stop the node REPL from appearing broke in recent versions of the JS DAP that started passing `--experimental-network-inspection` by default - we had lost the ability to create a debug terminal without specifying a program This PR fixes those issues. We also fixed environment variables from the **runInTerminal** request not getting passed to the spawned program. Release Notes: - Debugger: Fix RunInTerminal not working for JavaScript debugger. --------- Co-authored-by: Cole Miller --- crates/assistant_tools/src/terminal_tool.rs | 2 +- crates/dap_adapters/src/javascript.rs | 13 ++- crates/debugger_ui/src/session/running.rs | 87 +++++++++++-------- crates/extension_host/src/wasm_host/wit.rs | 2 +- .../src/wasm_host/wit/since_v0_6_0.rs | 12 +-- crates/project/src/debugger/locators/cargo.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/terminals.rs | 19 ++-- crates/proto/proto/debugger.proto | 2 +- crates/task/src/shell_builder.rs | 26 +++--- crates/task/src/task.rs | 2 +- crates/task/src/task_template.rs | 8 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/vim/src/command.rs | 2 +- 14 files changed, 108 insertions(+), 73 deletions(-) diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 2c582a531069eb9a81340af7eb07731e8df8a96e..9a3eac907cbdd6848df32eeed9481058bc368840 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -218,7 +218,7 @@ impl Tool for TerminalTool { .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task::SpawnInTerminal { - command: program, + command: Some(program), args, cwd, env, diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index d261d3b8b6e88c3b8069935caa9e3fd2b9d2d836..76c1d1fb7bb3b2b3a534293957b43919a079a888 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,9 +1,10 @@ use adapters::latest_github_release; use anyhow::Context as _; +use collections::HashMap; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use serde_json::Value; -use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; +use std::{path::PathBuf, sync::OnceLock}; use task::DebugRequest; use util::{ResultExt, maybe}; @@ -70,6 +71,8 @@ impl JsDebugAdapter { let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; + let mut envs = HashMap::default(); + let mut configuration = task_definition.config.clone(); if let Some(configuration) = configuration.as_object_mut() { maybe!({ @@ -110,6 +113,12 @@ impl JsDebugAdapter { } } + if let Some(env) = configuration.get("env").cloned() { + if let Ok(env) = serde_json::from_value(env) { + envs = env; + } + } + configuration .entry("cwd") .or_insert(delegate.worktree_root_path().to_string_lossy().into()); @@ -158,7 +167,7 @@ impl JsDebugAdapter { ), arguments, cwd: Some(delegate.worktree_root_path().to_path_buf()), - envs: HashMap::default(), + envs, connection: Some(adapters::TcpArguments { host, port, diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index b9f373daa4b6afc96e63817d64b686840a2d0738..af8c14aef77d0886071dfd899d8de5adff0d3ed6 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -973,7 +973,7 @@ impl RunningState { let task_with_shell = SpawnInTerminal { command_label, - command, + command: Some(command), args, ..task.resolved.clone() }; @@ -1085,19 +1085,6 @@ impl RunningState { .map(PathBuf::from) .or_else(|| session.binary().unwrap().cwd.clone()); - let mut args = request.args.clone(); - - // Handle special case for NodeJS debug adapter - // If only the Node binary path is provided, we set the command to None - // This prevents the NodeJS REPL from appearing, which is not the desired behavior - // The expected usage is for users to provide their own Node command, e.g., `node test.js` - // This allows the NodeJS debug client to attach correctly - let command = if args.len() > 1 { - Some(args.remove(0)) - } else { - None - }; - let mut envs: HashMap = self.session.read(cx).task_context().project_env.clone(); if let Some(Value::Object(env)) = &request.env { @@ -1111,32 +1098,58 @@ impl RunningState { } } - let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); - let kind = if let Some(command) = command { - let title = request.title.clone().unwrap_or(command.clone()); - TerminalKind::Task(task::SpawnInTerminal { - id: task::TaskId("debug".to_string()), - full_label: title.clone(), - label: title.clone(), - command: command.clone(), - args, - command_label: title.clone(), - cwd, - env: envs, - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: task::RevealStrategy::NoFocus, - reveal_target: task::RevealTarget::Dock, - hide: task::HideStrategy::Never, - shell, - show_summary: false, - show_command: false, - show_rerun: false, - }) + let mut args = request.args.clone(); + let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") { + // Handle special case for NodeJS debug adapter + // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection), + // we set the command to None + // This prevents the NodeJS REPL from appearing, which is not the desired behavior + // The expected usage is for users to provide their own Node command, e.g., `node test.js` + // This allows the NodeJS debug client to attach correctly + if args + .iter() + .filter(|arg| !arg.starts_with("--")) + .collect::>() + .len() + > 1 + { + Some(args.remove(0)) + } else { + None + } + } else if args.len() > 0 { + Some(args.remove(0)) } else { - TerminalKind::Shell(cwd.map(|c| c.to_path_buf())) + None }; + let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let title = request + .title + .clone() + .filter(|title| !title.is_empty()) + .or_else(|| command.clone()) + .unwrap_or_else(|| "Debug terminal".to_string()); + let kind = TerminalKind::Task(task::SpawnInTerminal { + id: task::TaskId("debug".to_string()), + full_label: title.clone(), + label: title.clone(), + command: command.clone(), + args, + command_label: title.clone(), + cwd, + env: envs, + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: task::RevealStrategy::NoFocus, + reveal_target: task::RevealTarget::Dock, + hide: task::HideStrategy::Never, + shell, + show_summary: false, + show_command: false, + show_rerun: false, + }); + let workspace = self.workspace.clone(); let weak_project = project.downgrade(); diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index b2b7726a1566dd202f25f52c3ceb8023ff216371..1f1fa49bd535ad19f4981eeed9fcdca1ba9421a9 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -999,7 +999,7 @@ impl Extension { ) -> Result> { match self { Extension::V0_6_0(ext) => { - let build_config_template = resolved_build_task.into(); + let build_config_template = resolved_build_task.try_into()?; let dap_request = ext .call_run_dap_locator(store, &locator_name, &build_config_template) .await? diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index f8f9ae1977687296790a562711c286e2fce026e4..ced2ea9c677022e95f106ac6ba0543303fe5a372 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -299,15 +299,17 @@ impl From for DebugScenario { } } -impl From for ResolvedTask { - fn from(value: SpawnInTerminal) -> Self { - Self { +impl TryFrom for ResolvedTask { + type Error = anyhow::Error; + + fn try_from(value: SpawnInTerminal) -> Result { + Ok(Self { label: value.label, - command: value.command, + command: value.command.context("missing command")?, args: value.args, env: value.env.into_iter().collect(), cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()), - } + }) } } diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index bad7dfe9f8947810346c06493d47d5f0b4c89c22..7d70371380192c99e1ace9676b02088f86ed9e5f 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -119,7 +119,7 @@ impl DapLocator for CargoLocator { .context("Couldn't get cwd from debug config which is needed for locators")?; let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); let (program, args) = builder.build( - "cargo".into(), + Some("cargo".into()), &build_config .args .iter() diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index b5eb62c4eb75e2165deaab9044d7b96f4c5e1f57..779cf95add9ad5547e13d85d87c0dcc3935ab326 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -568,7 +568,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { .into_iter() .map(|(source_kind, task)| { let resolved = task.resolved; - (source_kind, resolved.command) + (source_kind, resolved.command.unwrap()) }) .collect::>(), vec![( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b4e1093293b6275b9da68075425dd3b75b5bb335..b067396881d3b1bc0c20d8b0f21cb5ea80b675f9 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -149,7 +149,7 @@ impl Project { let settings = self.terminal_settings(&path, cx).clone(); let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); - let (command, args) = builder.build(command, &Vec::new()); + let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self .environment @@ -297,7 +297,10 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( &ssh_command, - Some((&spawn_task.command, &spawn_task.args)), + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), path.as_deref(), env, python_venv_directory.as_deref(), @@ -317,14 +320,16 @@ impl Project { add_environment_path(&mut env, &venv_path.join("bin")).log_err(); } - ( - task_state, + let shell = if let Some(program) = spawn_task.command { Shell::WithArguments { - program: spawn_task.command, + program, args: spawn_task.args, title_override: None, - }, - ) + } + } else { + Shell::System + }; + (task_state, shell) } } } diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index 3979265accaa07040373174a4e7984d181a1da33..09abd4bf1c1aa73e89d77c55ade1bce21f0027d4 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -535,7 +535,7 @@ message DebugScenario { message SpawnInTerminal { string label = 1; - string command = 2; + optional string command = 2; repeated string args = 3; map env = 4; optional string cwd = 5; diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index c75aa059f0e4571093ce97fec31860af3c8c4652..544663713933dd967f71c9330268f46688b11d93 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -149,17 +149,23 @@ impl ShellBuilder { } } /// Returns the program and arguments to run this task in a shell. - pub fn build(mut self, task_command: String, task_args: &Vec) -> (String, Vec) { - let combined_command = task_args - .into_iter() - .fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); + pub fn build( + mut self, + task_command: Option, + task_args: &Vec, + ) -> (String, Vec) { + if let Some(task_command) = task_command { + let combined_command = task_args + .into_iter() + .fold(task_command, |mut command, arg| { + command.push(' '); + command.push_str(&self.kind.to_shell_variable(arg)); + command + }); - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); + self.args + .extend(self.kind.args_for_shell(self.interactive, combined_command)); + } (self.program, self.args) } diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index bbae4850c16d5e7b7caacde245dc36429c18ed8e..aae28ab874544f683bf48f873d4a9a80a529a32b 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -46,7 +46,7 @@ pub struct SpawnInTerminal { /// Human readable name of the terminal tab. pub label: String, /// Executable command to spawn. - pub command: String, + pub command: Option, /// Arguments to the command, potentially unsubstituted, /// to let the shell that spawns the command to do the substitution, if needed. pub args: Vec, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index cc36b28e4beebc230cd635b894c5288cfcd3ada4..ae5054ac556b4ad82f5c9243005592593b033006 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -255,7 +255,7 @@ impl TaskTemplate { command_label }, ), - command, + command: Some(command), args: self.args.clone(), env, use_new_terminal: self.use_new_terminal, @@ -635,7 +635,7 @@ mod tests { "Human-readable label should have long substitutions trimmed" ); assert_eq!( - spawn_in_terminal.command, + spawn_in_terminal.command.clone().unwrap(), format!("echo test_file {long_value}"), "Command should be substituted with variables and those should not be shortened" ); @@ -652,7 +652,7 @@ mod tests { spawn_in_terminal.command_label, format!( "{} arg1 test_selected_text arg2 5678 arg3 {long_value}", - spawn_in_terminal.command + spawn_in_terminal.command.clone().unwrap() ), "Command label args should be substituted with variables and those should not be shortened" ); @@ -711,7 +711,7 @@ mod tests { assert_substituted_variables(&resolved_task, Vec::new()); let resolved = resolved_task.resolved; assert_eq!(resolved.label, task.label); - assert_eq!(resolved.command, task.command); + assert_eq!(resolved.command, Some(task.command)); assert_eq!(resolved.args, task.args); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 8c55fed2a60127db4dd6fd0845f219507a8f4f78..f6eee3065ca974449315ab2ac519de1acb5da11e 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -505,7 +505,7 @@ impl TerminalPanel { let task = SpawnInTerminal { command_label, - command, + command: Some(command), args, ..task.clone() }; diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 729e1a7b3c008c957f3f018f79bdcccf78a8b698..b24ca75e8bc1f922a86b011c9dcfc27a92b57e47 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1688,7 +1688,7 @@ impl ShellExec { id: TaskId("vim".to_string()), full_label: command.clone(), label: command.clone(), - command: command.clone(), + command: Some(command.clone()), args: Vec::new(), command_label: command.clone(), cwd,