From 542584abb8b65f208b3c8fca3bf1bf1f293bb9e6 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:52:41 +0800 Subject: [PATCH] feat(debug): debug programs in a plugin run target (#2264) --- Cargo.lock | 3 + crates/ide-dap/Cargo.toml | 2 + crates/ide-dap/src/adapter.rs | 5 +- crates/ide-dap/src/connection.rs | 26 +++- crates/ide-dap/src/lib.rs | 61 +++++++- crates/ide-dap/src/session.rs | 4 + crates/ide-dap/src/target.rs | 72 ++++++++++ crates/ide-dap/src/tests.rs | 1 + .../ide-dap/tests/fixtures/target_adapter.py | 30 ++++ crates/ide-dap/tests/php_debug.rs | 20 ++- crates/ide-dap/tests/target_debug.rs | 130 ++++++++++++++++++ crates/ide-host/Cargo.toml | 1 + crates/ide-host/src/ops/debug.rs | 10 ++ crates/ide-host/src/ops/debug/target.rs | 102 ++++++++++++++ crates/ide-host/src/ops/lsp.rs | 38 ++++- crates/ide-host/tests/target_debug.rs | 102 ++++++++++++++ crates/ide-language/src/lib.rs | 3 + .../fixtures/api14/dist/plugin.wasm.sources | 2 +- .../fixtures/commit/dist/plugin.wasm.sources | 2 +- .../data-tools/dist/plugin.wasm.sources | 2 +- .../detail-views/dist/plugin.wasm.sources | 2 +- .../diagnostics/dist/plugin.wasm.sources | 2 +- .../endpoints/dist/plugin.wasm.sources | 2 +- .../execution/dist/plugin.wasm.sources | 2 +- .../file-templates/dist/plugin.wasm.sources | 2 +- .../files-events/dist/plugin.wasm.sources | 2 +- .../grouped-views/dist/plugin.wasm.sources | 2 +- .../fixtures/hello/dist/plugin.wasm.sources | 2 +- .../fixtures/hover/dist/plugin.wasm.sources | 2 +- .../fixtures/indexer/dist/plugin.wasm.sources | 2 +- .../inlay-hints/dist/plugin.wasm.sources | 2 +- .../fixtures/jobs/dist/plugin.wasm.sources | 2 +- .../dist/plugin.wasm.sources | 2 +- .../misbehave/dist/plugin.wasm.sources | 2 +- .../path-events/dist/plugin.wasm.sources | 2 +- .../private-dir/dist/plugin.wasm.sources | 2 +- .../semantic-tokens/dist/plugin.wasm.sources | 2 +- .../settings-pages/dist/plugin.wasm.sources | 2 +- .../fixtures/storage/dist/plugin.wasm.sources | 2 +- .../fixtures/symbols/dist/plugin.wasm.sources | 2 +- .../fixtures/tables/dist/plugin.wasm.sources | 2 +- .../web-content/dist/plugin.wasm.sources | 2 +- .../fixtures/web-log/dist/plugin.wasm.sources | 2 +- .../fixtures/web-pty/dist/plugin.wasm.sources | 2 +- languages/php.toml | 1 + languages/python.toml | 1 + plugins/README.md | 16 +++ .../ai-completion/dist/plugin.wasm.sources | 2 +- .../dist/plugin.wasm.sources | 2 +- .../checkstyle/dist/plugin.wasm.sources | 2 +- .../container-runner/dist/plugin.wasm.sources | 2 +- .../csv-editor/dist/plugin.wasm.sources | 2 +- .../git-toolbox/dist/plugin.wasm.sources | 2 +- .../graphviz/dist/plugin.wasm.sources | 2 +- .../key-promoter/dist/plugin.wasm.sources | 2 +- .../license-header/dist/plugin.wasm.sources | 2 +- .../official/mybatis/dist/plugin.wasm.sources | 2 +- .../regex-tester/dist/plugin.wasm.sources | 2 +- .../solidity/dist/plugin.wasm.sources | 2 +- .../string-tools/dist/plugin.wasm.sources | 2 +- .../test-runner/dist/plugin.wasm.sources | 2 +- .../todo-finder/dist/plugin.wasm.sources | 2 +- sdk/rust/openidea-plugin/wit/run-targets.wit | 5 +- src/features/debugger/actions.ts | 20 ++- src/features/run/index.ts | 6 + src/features/run/remote.test.tsx | 8 ++ src/features/run/remote.ts | 3 +- src/platform/locales/text/en.json | 6 +- src/platform/locales/text/zh.json | 6 +- wit/run-targets.wit | 5 +- 70 files changed, 709 insertions(+), 62 deletions(-) create mode 100644 crates/ide-dap/src/target.rs create mode 100644 crates/ide-dap/tests/fixtures/target_adapter.py create mode 100644 crates/ide-dap/tests/target_debug.rs create mode 100644 crates/ide-host/src/ops/debug/target.rs create mode 100644 crates/ide-host/tests/target_debug.rs diff --git a/Cargo.lock b/Cargo.lock index 8c44c1bc0..4aa0033d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4306,6 +4306,8 @@ dependencies = [ "ide-core", "ide-jsonrpc", "ide-language", + "ide-remote", + "ide-run", "libc", "memchr", "parking_lot", @@ -4453,6 +4455,7 @@ dependencies = [ "ignore", "jsonschema", "parking_lot", + "percent-encoding", "quick-xml 0.42.0", "regex-lite", "regex-syntax", diff --git a/crates/ide-dap/Cargo.toml b/crates/ide-dap/Cargo.toml index 31fc65f4f..b69732d7e 100644 --- a/crates/ide-dap/Cargo.toml +++ b/crates/ide-dap/Cargo.toml @@ -24,5 +24,7 @@ tracing.workspace = true libc = "0.2" [dev-dependencies] +ide-remote.workspace = true +ide-run.workspace = true tracing-subscriber.workspace = true tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros", "test-util"] } diff --git a/crates/ide-dap/src/adapter.rs b/crates/ide-dap/src/adapter.rs index e74fa3380..9c8913634 100644 --- a/crates/ide-dap/src/adapter.rs +++ b/crates/ide-dap/src/adapter.rs @@ -31,6 +31,7 @@ pub enum Transport { /// A fully resolved adapter command line. #[derive(Clone, Debug)] pub struct AdapterSpec { + pub target: Option, pub adapter: String, pub command: String, pub args: Vec, @@ -51,6 +52,7 @@ pub struct AdapterSpec { /// Built-in adapters used when no language definition names `adapter`. fn builtin(adapter: &str) -> Option { let cfg = |command: &str, args: &[&str], transport: &str, hint: &str| DebuggerConfig { + target_placement: None, adapter: adapter.to_owned(), command: command.to_owned(), args: args.iter().map(|a| (*a).to_owned()).collect(), @@ -134,6 +136,7 @@ pub fn default_type(adapter: &str) -> Option<&'static str> { pub fn resolve(config: &DebuggerConfig, cwd: PathBuf) -> AdapterSpec { AdapterSpec { + target: None, adapter: config.adapter.clone(), command: config.command.clone(), args: config.args.clone(), @@ -447,7 +450,7 @@ impl AdapterLauncher for ProcessLauncher { program_output: None, }); } - if spec.adapter == DEBUGPY_TOOL { + if spec.adapter == DEBUGPY_TOOL && spec.target.is_none() { prepare_debugpy(&mut spec).await?; } if is_plugin_bundle(&spec.command) { diff --git a/crates/ide-dap/src/connection.rs b/crates/ide-dap/src/connection.rs index ed5423051..619f2a8f9 100644 --- a/crates/ide-dap/src/connection.rs +++ b/crates/ide-dap/src/connection.rs @@ -71,6 +71,7 @@ pub enum Incoming { type Pending = HashMap>)>; struct Inner { + mapping: Option, out: mpsc::UnboundedSender>, pending: Mutex, seq: AtomicI64, @@ -106,9 +107,22 @@ impl Connection { /// [`start`](Self::start) that hands lines outside the protocol (an adapter's /// own output on stdout, decoded) to `on_output`. pub fn start_with_output( + reader: R, + writer: W, + on_output: Box, + ) -> (Self, mpsc::UnboundedReceiver) + where + R: AsyncRead + Unpin + Send + 'static, + W: AsyncWrite + Unpin + Send + 'static, + { + Self::start_with_target(reader, writer, on_output, None) + } + + pub fn start_with_target( reader: R, mut writer: W, on_output: Box, + target: Option, ) -> (Self, mpsc::UnboundedReceiver) where R: AsyncRead + Unpin + Send + 'static, @@ -117,6 +131,7 @@ impl Connection { let (out_tx, mut out_rx) = mpsc::unbounded_channel::>(); let (in_tx, in_rx) = mpsc::unbounded_channel(); let inner = Arc::new(Inner { + mapping: target, out: out_tx, pending: Mutex::new(HashMap::new()), seq: AtomicI64::new(1), @@ -187,7 +202,11 @@ impl Connection { if self.is_closed() { return Err(DapError::Closed); } - let body = serde_json::to_vec(value).map_err(|e| DapError::Protocol(e.to_string()))?; + let mut value = value.clone(); + if let Some(mapping) = self.inner.mapping.as_ref() { + mapping.rewrite(&mut value, ide_core::path_mapping::Direction::ToTarget); + } + let body = serde_json::to_vec(&value).map_err(|e| DapError::Protocol(e.to_string()))?; tracing::trace!(target: "ide_dap::wire", "--> {}", String::from_utf8_lossy(&body)); self.inner.out.send(body).map_err(|_| DapError::Closed) } @@ -246,13 +265,16 @@ impl Connection { fn dispatch(inner: &Inner, incoming: &mpsc::UnboundedSender, body: &[u8]) { tracing::trace!(target: "ide_dap::wire", "<-- {}", String::from_utf8_lossy(body)); - let msg: Value = match serde_json::from_slice(body) { + let mut msg: Value = match serde_json::from_slice(body) { Ok(v) => v, Err(e) => { tracing::warn!("dap: unparsable message: {e}"); return; } }; + if let Some(mapping) = inner.mapping.as_ref() { + mapping.rewrite(&mut msg, ide_core::path_mapping::Direction::ToHost); + } match msg.get("type").and_then(Value::as_str) { Some("response") => { let Some(request_seq) = msg.get("request_seq").and_then(Value::as_i64) else { diff --git a/crates/ide-dap/src/lib.rs b/crates/ide-dap/src/lib.rs index c123e4d8d..fea80fbe3 100644 --- a/crates/ide-dap/src/lib.rs +++ b/crates/ide-dap/src/lib.rs @@ -16,6 +16,7 @@ pub mod jdwp; pub mod kotlin; mod kotlin_jdwp; mod session; +pub mod target; use adapter::{AdapterLauncher, AdapterSpec, ProcessLauncher}; use connection::{Connection, DapError}; @@ -696,6 +697,17 @@ impl DebugManager { config: LaunchConfig, owner: SessionOwner, custom_command: impl FnOnce(&str) -> Option, + ) -> IdeResult { + self.start_prepared(config, owner, custom_command, |_, _| Ok(())).await + } + + /// Configure a target adapter before any host tool resolution or launch. + pub async fn start_prepared( + &self, + config: LaunchConfig, + owner: SessionOwner, + custom_command: impl FnOnce(&str) -> Option, + prepare: impl FnOnce(&mut AdapterSpec, &mut Value) -> IdeResult<()>, ) -> IdeResult { let mut arguments = launch_arguments(&config)?; // An IDE-side marker (never sent to the adapter): the session is a Flutter @@ -710,6 +722,22 @@ impl DebugManager { let dbg = adapter::find_config(&self.languages, &config.adapter) .ok_or_else(|| IdeError::unavailable(format!("unknown debug adapter `{}`", config.adapter)))?; let mut spec = adapter::resolve(&dbg, cwd.clone()); + prepare(&mut spec, &mut arguments)?; + if spec.target.is_some() { + return self + .start_session( + DebugSessionId::next(), + owner, + config.name, + config.adapter, + None, + config.request, + arguments, + spec, + None, + ) + .await; + } if config.adapter == kotlin::ADAPTER && let Some(home) = arguments .as_object_mut() @@ -858,11 +886,22 @@ impl DebugManager { if request != "launch" && request != "attach" { return Err(IdeError::invalid(format!("request must be `launch` or `attach`, got `{request}`"))); } - let io = self.launcher.launch(spec.clone(), reuse_port).await?; + let io = self.launcher.launch(spec.clone(), reuse_port).await.map_err(|e| { + if spec.target.is_some() { + IdeError::new(e.kind, format!("{}\nargv: {:?} {:?}", e.message, spec.command, spec.args)) + } else { + e + } + })?; let stderr = io.stderr.clone(); let (conn, incoming) = { let stderr = stderr.clone(); - Connection::start_with_output(io.reader, io.writer, Box::new(move |line| adapter::push_line(&stderr, line))) + Connection::start_with_target( + io.reader, + io.writer, + Box::new(move |line| adapter::push_line(&stderr, line)), + spec.target.clone(), + ) }; let rdbg_pid = (adapter_name == "rdbg" && request == "launch") .then(|| io.child.as_ref().and_then(tokio::process::Child::id)) @@ -903,6 +942,9 @@ impl DebugManager { Err(e) => { let e = if session.conn.is_closed() { let mut msg = format!("debug adapter `{}` exited during startup", session.adapter); + if session.spec.target.is_some() { + msg.push_str(&format!("\nargv: {:?} {:?}", session.spec.command, session.spec.args)); + } let tail = stderr.lock().join("\n"); if !tail.is_empty() { msg.push_str(":\n"); @@ -913,6 +955,17 @@ impl DebugManager { msg.push_str(hint); } IdeError::unavailable(msg) + } else if session.spec.target.is_some() { + IdeError::new( + e.kind, + format!( + "{}\nargv: {:?} {:?}\n{}", + e.message, + session.spec.command, + session.spec.args, + stderr.lock().join("\n") + ), + ) } else { e }; @@ -950,8 +1003,8 @@ impl DebugManager { "pathFormat": "path", "supportsVariableType": true, "supportsVariablePaging": false, - "supportsRunInTerminalRequest": true, - "supportsStartDebuggingRequest": true, + "supportsRunInTerminalRequest": s.spec.target.is_none(), + "supportsStartDebuggingRequest": s.spec.target.is_none(), "supportsProgressReporting": true, "supportsInvalidatedEvent": false, "supportsMemoryReferences": false, diff --git a/crates/ide-dap/src/session.rs b/crates/ide-dap/src/session.rs index 0bf73f0b0..0acc18901 100644 --- a/crates/ide-dap/src/session.rs +++ b/crates/ide-dap/src/session.rs @@ -579,6 +579,10 @@ async fn on_event(mgr: &Arc, s: &Arc, event: &str, mut bo } fn on_reverse_request(mgr: &Arc, s: &Arc, seq: i64, command: &str, args: Value) { + if s.spec.target.is_some() { + let _ = s.conn.respond(seq, command, Err("This debugger does not support plugin run targets".into())); + return; + } match command { "runInTerminal" => { let result = run_in_terminal(s, &args); diff --git a/crates/ide-dap/src/target.rs b/crates/ide-dap/src/target.rs new file mode 100644 index 000000000..705948392 --- /dev/null +++ b/crates/ide-dap/src/target.rs @@ -0,0 +1,72 @@ +//! DAP path fields only: output, expressions, environment and program arguments are opaque. +use ide_core::path_mapping::{Direction, PathMapping, file_path, file_uri}; +use serde_json::Value; + +#[derive(Clone, Debug)] +pub struct TargetMapping { + pub paths: PathMapping, + pub external_prefix: String, +} + +impl TargetMapping { + pub fn rewrite(&self, value: &mut Value, direction: Direction) { + match value { + Value::Array(items) => items.iter_mut().for_each(|v| self.rewrite(v, direction)), + Value::Object(fields) => { + for (key, value) in fields { + if key == "pathMappings" { + continue; + } + if matches!(key.as_str(), "path" | "program" | "cwd" | "localRoot" | "remoteRoot") { + if let Some(path) = value.as_str() { + if matches!(direction, Direction::ToTarget) && path.starts_with(&self.external_prefix) { + if let Some(decoded) = + file_path(&format!("file:///{}", path.trim_start_matches(&self.external_prefix))) + { + *value = decoded.into(); + } + } else if let Some(mapped) = self.paths.map(path, direction) { + *value = mapped.into(); + } else if matches!(direction, Direction::ToHost) && key == "path" && path.starts_with('/') { + *value = format!( + "{}{}", + self.external_prefix, + file_uri(path).trim_start_matches("file:///") + ) + .into(); + } + } + } else if !matches!(key.as_str(), "args" | "env" | "output" | "expression" | "value") { + self.rewrite(value, direction); + } + } + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + #[test] + fn maps_protocol_paths_without_changing_user_data_or_adapter_mappings() { + let m = TargetMapping { + paths: PathMapping::new("C:/project", "/app"), + external_prefix: "openidea-target://test/0/".into(), + }; + let mut request = json!({"arguments":{"source":{"path":"C:/project/a.py"},"program":"C:/project/a.py","cwd":"C:/project", "args":["C:/project/text"],"env":{"path":"C:/project/text"},"pathMappings":[{"localRoot":"/app","remoteRoot":"/app"}]}}); + m.rewrite(&mut request, Direction::ToTarget); + assert_eq!(request["arguments"]["source"]["path"], "/app/a.py"); + assert_eq!(request["arguments"]["cwd"], "/app"); + assert_eq!(request["arguments"]["args"][0], "C:/project/text"); + assert_eq!(request["arguments"]["env"]["path"], "C:/project/text"); + let mut response = json!({"body":{"stackFrames":[{"source":{"path":"/app/a.py"}},{"source":{"path":"/usr/lib/a.py"}}],"sources":[{"path":"/app/b.py"}],"output":"/app/a.py"}}); + m.rewrite(&mut response, Direction::ToHost); + assert_eq!(response["body"]["stackFrames"][0]["source"]["path"], "C:\\project\\a.py"); + assert_eq!(response["body"]["sources"][0]["path"], "C:\\project\\b.py"); + assert_eq!(response["body"]["stackFrames"][1]["source"]["path"], "openidea-target://test/0/usr/lib/a.py"); + assert_eq!(response["body"]["output"], "/app/a.py"); + } +} diff --git a/crates/ide-dap/src/tests.rs b/crates/ide-dap/src/tests.rs index bb00ba2c7..5e490d066 100644 --- a/crates/ide-dap/src/tests.rs +++ b/crates/ide-dap/src/tests.rs @@ -1166,6 +1166,7 @@ fn godot_registry(port: u16, command: &str, args: &[&str]) -> Arc wrapper-argv\nexec \"$@\"\n").unwrap(); + std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap(); + let target = ide_core::remote_settings::PluginRunTarget { + id: "fake".into(), + name: "Fake".into(), + program: "wrapper".into(), + args: vec![], + workdir_args: vec![], + container_root: Some("/app".into()), + commands: vec![], + }; + // Keep the fixture adapter outside mapped argv; this is the container's executable. + let run: ide_run::RunConfig = serde_json::from_value(serde_json::json!({"id":"adapter","name":"Adapter","kind":"shell","command":"/usr/bin/python3","args":["-u","-c",ADAPTER],"cwd":".","env":{},"detected":false,"isTest":false})).unwrap(); + let run = ide_remote::plugin_target::prepare(&root, &target, &wrapper, &run).unwrap(); + let manager = DebugManager::new(Arc::new(LanguageRegistry::load(None).unwrap())); + manager + .set_breakpoints( + root.join("main.py"), + vec![Breakpoint { line: 2, enabled: true, condition: None, hit_condition: None, log_message: None }], + ) + .await + .unwrap(); + let config = LaunchConfig { + name: "Target".into(), + adapter: "debugpy".into(), + request: "launch".into(), + program: Some(root.join("main.py").to_string_lossy().into_owned()), + args: vec![], + cwd: Some(root.clone().into()), + env: BTreeMap::new(), + extra_json: None, + }; + let sink: SharedSink = Arc::new(|_| {}); + let mapping = TargetMapping { + paths: PathMapping::new(&root.to_string_lossy(), "/app"), + external_prefix: "openidea-target://fake/0/".into(), + }; + let id = manager + .start_prepared( + config, + SessionOwner::new("test", sink), + |_| None, + move |spec, _| { + spec.command = run.command; + spec.args = run.args; + spec.cwd = root; + spec.target = Some(mapping); + Ok(()) + }, + ) + .await + .unwrap(); + let frames = manager.stack_trace(id, 1).await.unwrap(); + assert_eq!( + frames[0].path.as_ref().unwrap().to_string_lossy(), + tmp.path().canonicalize().unwrap().join("main.py").to_string_lossy() + ); + assert_eq!(frames[0].line, 2); + assert!(std::fs::read_to_string(tmp.path().join("wrapper-argv")).unwrap().starts_with("python3\n-u\n-c\n")); + manager.stop(id).await.unwrap(); +} + +#[tokio::test] +async fn host_adapter_accepts_connect_back_from_target_with_mapped_sources() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().canonicalize().unwrap(); + let port = ide_dap::adapter::free_port().unwrap(); + let manager = DebugManager::new(Arc::new(LanguageRegistry::load(None).unwrap())); + manager + .set_breakpoints( + root.join("main.py"), + vec![Breakpoint { line: 2, enabled: true, condition: None, hit_condition: None, log_message: None }], + ) + .await + .unwrap(); + let config = LaunchConfig { + name: "Connect back".into(), + adapter: "php-debug".into(), + request: "launch".into(), + program: None, + args: vec![], + cwd: Some(root.clone().into()), + env: BTreeMap::new(), + extra_json: Some(serde_json::json!({"port":port,"pathMappings":{"/app":"/app"}}).to_string()), + }; + let sink: SharedSink = Arc::new(|_| {}); + let mapping = TargetMapping { + paths: PathMapping::new(&root.to_string_lossy(), "/app"), + external_prefix: "openidea-target://fake/0/".into(), + }; + let id = manager + .start_prepared( + config, + SessionOwner::new("test", sink), + |_| None, + move |spec, _| { + spec.command = "/usr/bin/python3".into(); + spec.args = vec!["-u".into(), "-c".into(), ADAPTER.into()]; + spec.env.push(("FAKE_CONNECT_BACK".into(), "1".into())); + spec.target = Some(mapping); + Ok(()) + }, + ) + .await + .unwrap(); + let code = + format!("import socket;s=socket.create_connection(('127.0.0.1', {port}));s.sendall(b'/app/main.py');s.close()"); + let mut child = tokio::process::Command::new("/usr/bin/python3").args(["-c", &code]).spawn().unwrap(); + let frames = manager.stack_trace(id, 1).await.unwrap(); + assert_eq!(frames[0].path.as_ref().unwrap().to_path_buf(), root.join("main.py")); + assert!(child.wait().await.unwrap().success()); + manager.stop(id).await.unwrap(); +} diff --git a/crates/ide-host/Cargo.toml b/crates/ide-host/Cargo.toml index 730e8f766..21cb5602c 100644 --- a/crates/ide-host/Cargo.toml +++ b/crates/ide-host/Cargo.toml @@ -10,6 +10,7 @@ name = "openidea-server" path = "src/bin/server.rs" [dependencies] +percent-encoding = "2" encoding_rs.workspace = true ide-core.workspace = true ide-mcp.workspace = true diff --git a/crates/ide-host/src/ops/debug.rs b/crates/ide-host/src/ops/debug.rs index a0bd5889b..89dcd1a92 100644 --- a/crates/ide-host/src/ops/debug.rs +++ b/crates/ide-host/src/ops/debug.rs @@ -1,4 +1,5 @@ mod output; +mod target; use crate::Host; use crate::rpc::Dispatcher; @@ -9,6 +10,15 @@ use std::path::PathBuf; /// Starts a session owned by this window: its events (and those of child sessions) go here only. pub async fn debug_start(host: &Host, window: &str, config: LaunchConfig) -> IdeResult { host.require_trust(window)?; + let target_id = config + .extra_json + .as_deref() + .and_then(|s| serde_json::from_str::(s).ok()) + .and_then(|v| v.get("__runTarget").and_then(|v| v.as_str()).map(str::to_owned)); + if let Some(id) = target_id { + let owner = SessionOwner::new(host.owner_window(window), host.window_sink(window)); + return target::start(host, window, config, owner, id).await; + } let mut config = match_java_debug_encoding(config, cfg!(windows)); if config.adapter == "debugpy" && config.request == "launch" diff --git a/crates/ide-host/src/ops/debug/target.rs b/crates/ide-host/src/ops/debug/target.rs new file mode 100644 index 000000000..2dfb4e9cd --- /dev/null +++ b/crates/ide-host/src/ops/debug/target.rs @@ -0,0 +1,102 @@ +use crate::Host; +use ide_core::path_mapping::PathMapping; +use ide_core::{IdeError, IdeResult}; +use ide_dap::target::TargetMapping; +use ide_dap::{LaunchConfig, SessionOwner}; +use ide_language::ProgramPin; +use serde_json::json; + +pub(super) async fn start( + host: &Host, + window: &str, + mut config: LaunchConfig, + owner: SessionOwner, + id: String, +) -> IdeResult { + let project = host.project(window)?; + let plugins = + host.plugin_host().ok_or_else(|| IdeError::unavailable("plugin run target is no longer available"))?; + let registration = plugins + .run_targets(&project.root) + .into_iter() + .find(|t| t.id == id) + .ok_or_else(|| IdeError::unavailable("plugin run target is no longer available"))?; + let container = registration + .target + .container_root + .ok_or_else(|| IdeError::invalid("Debugging a plugin target requires a container root"))?; + let dbg = ide_dap::adapter::find_config(&host.languages, &config.adapter) + .ok_or_else(|| IdeError::invalid("unknown debug adapter"))?; + let placement = dbg.target_placement.as_deref().unwrap_or(""); + if !matches!(placement, "connect-back" | "adapter-in-target") + || (placement == "adapter-in-target" && dbg.transport != "stdio") + { + return Err(IdeError::invalid("This debugger does not support plugin run targets")); + } + let mapping = TargetMapping { + paths: PathMapping::new(&project.root.to_string_lossy(), &container), + external_prefix: format!( + "openidea-target://{}/0/", + percent_encoding::utf8_percent_encode(&id, percent_encoding::NON_ALPHANUMERIC) + ), + }; + let mut extra: serde_json::Value = config + .extra_json + .as_deref() + .map(serde_json::from_str) + .transpose() + .map_err(|e| IdeError::invalid(e.to_string()))? + .unwrap_or(json!({})); + extra.as_object_mut().ok_or_else(|| IdeError::invalid("invalid debug launch arguments"))?.remove("__runTarget"); + // DAP paths already use target coordinates; adapter-native mappings stay identity. + if placement == "connect-back" { + config.program = None; + config.args.clear(); + for key in ["program", "runtimeExecutable", "runtimeArgs"] { + extra.as_object_mut().unwrap().remove(key); + } + if extra.get("port").and_then(|v| v.as_u64()).unwrap_or(0) == 0 { + extra["port"] = json!(9003); + } + extra["hostname"] = json!("0.0.0.0"); + extra["pathMappings"] = json!({container.clone(): container.clone()}); + } else { + for key in ["debugLauncherPython", "pythonPath"] { + extra.as_object_mut().unwrap().remove(key); + } + extra["console"] = json!("internalConsole"); + extra["python"] = json!(dbg.command); + extra["pathMappings"] = json!([{"localRoot":container,"remoteRoot":container}]); + } + config.extra_json = Some(extra.to_string()); + let wrapped = if placement == "adapter-in-target" { + let run = serde_json::from_value(json!({"id":"debug-adapter","name":"Debug adapter","kind":"shell","command":dbg.command,"args":dbg.args,"cwd":config.cwd,"env":config.env,"detected":false,"isTest":false})).map_err(|e| IdeError::invalid(e.to_string()))?; + let root = project.root.clone(); + Some(host.blocking(move || plugins.prepare_run_target(&root, &id, &run)).await?) + } else { + None + }; + host.require_trust(window)?; + let root = project.root.clone(); + let settings = project.settings.effective(); + host.debugger + .start_prepared( + config, + owner, + |_| None, + move |spec, _| { + if let Some(run) = wrapped { + spec.command = run.command; + spec.args = run.args; + spec.cwd = run.cwd.map(Into::into).unwrap_or(root); + spec.env = run.env.into_iter().collect(); + spec.pinned = Some(ProgramPin::approve(&spec.command)?); + } else if let Some(command) = ide_sdk::apply::tool_command(&root, &settings, &spec.command) { + spec.command = command.to_string_lossy().into_owned(); + } + spec.target = Some(mapping); + Ok(()) + }, + ) + .await +} diff --git a/crates/ide-host/src/ops/lsp.rs b/crates/ide-host/src/ops/lsp.rs index b2a1faca4..e3ca31a0a 100644 --- a/crates/ide-host/src/ops/lsp.rs +++ b/crates/ide-host/src/ops/lsp.rs @@ -4,7 +4,7 @@ use crate::apply_workspace_edit as apply_edit; use crate::plugins::{ProviderKind, editor_providers_for as editor_providers}; use crate::rpc::Dispatcher; use ide_core::error::ErrorKind; -use ide_core::{DocId, IdeResult, Location, Pos, Range, RequestId, ServerId, TextEdit, WorkspaceEdit}; +use ide_core::{DocId, IdeError, IdeResult, Location, Pos, Range, RequestId, ServerId, TextEdit, WorkspaceEdit}; use ide_lsp::types::*; use ide_plugin::ProviderDocument; use std::path::Path; @@ -545,6 +545,42 @@ pub async fn lsp_virtual_document(host: &Host, window: &str, uri: String) -> Ide if uri.starts_with("git-revision:") { return crate::ops::vcs::revision_virtual_document(host, window, &uri).await; } + if uri.starts_with("openidea-target:") && uri.contains("/0/") { + host.require_trust(window)?; + let project = host.project(window)?; + let plugins = + host.plugin_host().ok_or_else(|| IdeError::unavailable("container document reader is unavailable"))?; + for registration in plugins.run_targets(&project.root) { + let prefix = format!( + "openidea-target://{}/0/", + percent_encoding::utf8_percent_encode(®istration.id, percent_encoding::NON_ALPHANUMERIC) + ); + if let Some(suffix) = uri.strip_prefix(&prefix) { + let path = ide_core::path_mapping::file_path(&format!("file:///{suffix}")) + .filter(|p| p.starts_with('/') && !p.split('/').any(|s| s == ".." || s == ".")) + .ok_or_else(|| IdeError::invalid("invalid container document URI"))?; + let container_root = registration + .target + .container_root + .ok_or_else(|| IdeError::invalid("invalid container document URI"))?; + let target = ide_language::ServerTarget { + id: registration.id, + origin: None, + project_root: project.root.clone(), + mapping: ide_core::path_mapping::PathMapping::new(&project.root.to_string_lossy(), &container_root), + container_root, + }; + let title = std::path::Path::new(&path) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + let language_id = host.languages.detect(std::path::Path::new(&path), None); + let text = host.blocking(move || super::lsp_target::read(&plugins, &target, &path)).await?; + return Ok(VirtualDocument { uri, text, title, language_id, description: None, decompiled: false }); + } + } + return Err(IdeError::unavailable("plugin run target is no longer available")); + } host.project(window)?.lsp.virtual_document(&uri).await } diff --git a/crates/ide-host/tests/target_debug.rs b/crates/ide-host/tests/target_debug.rs new file mode 100644 index 000000000..c3572f178 --- /dev/null +++ b/crates/ide-host/tests/target_debug.rs @@ -0,0 +1,102 @@ +//! Target debugging uses the same trusted plugin wrapper on desktop and the SSH server. +#![cfg(unix)] +use ide_core::IdeEvent; +use ide_host::{ + Host, HostConfig, + plugins::{Consent, HostWorkspace, PluginEvent, PluginHost}, +}; +use ide_language::LanguageRegistry; +use ide_plugin::{Invocation, Limits}; +use parking_lot::Mutex; +use serde_json::json; +use std::{os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; +#[tokio::test(flavor = "multi_thread")] +async fn registered_target_debugs_over_stdio_and_rechecks_program_grant() { + let tmp = tempfile::tempdir().unwrap(); + let base = dunce::canonicalize(tmp.path()).unwrap(); + let root = base.join("project"); + std::fs::create_dir(&root).unwrap(); + let adapter = base.join("adapter.py"); + std::fs::write(&adapter, include_str!("../../ide-dap/tests/fixtures/target_adapter.py")).unwrap(); + let wrapper = base.join("wrapper"); + std::fs::write( + &wrapper, + format!( + r#"#!/bin/sh +[ "$1" = wrapped ] && [ "$2" = --cwd=/container ] || exit 91 +shift 2 +if [ "$1" = cat ]; then exec cat "$2" "$3"; fi +export FAKE_ROOT=/container +exec /usr/bin/python3 -u '{}' +"#, + adapter.display() + ), + ) + .unwrap(); + std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap(); + let events = Arc::new(Mutex::new(Vec::new())); + let sink_events = events.clone(); + let host = Arc::new(Host::new(HostConfig { + config_dir: base.join("config"), + data_dir: base.join("data"), + local_data_dir: base.join("local"), + languages: Arc::new(LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(move |_| { + let e = sink_events.clone(); + Arc::new(move |event: IdeEvent| e.lock().push(event)) + }), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + host.open_project("main", &root).unwrap(); + host.set_project_trust(&root, true, false).await.unwrap(); + let plugins = Arc::new( + PluginHost::new(base.join("plugins"), Arc::new(|_: Option<&str>, _: PluginEvent| {}), Limits::default()) + .unwrap(), + ); + plugins.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host))))); + host.set_plugin_host(&plugins); + let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/execution"); + plugins.install_from_dir(&package, &Consent::all()).unwrap(); + plugins.choose_program("execution", "process:execution-tool", Some(&root), Some(&wrapper)).unwrap(); + let invocation = + || Invocation { client: Some("main".into()), workspace_root: Some(root.clone()), ..Default::default() }; + plugins.run_command("execution", "target", None, invocation()).unwrap(); + let id = plugins.run_targets(&root)[0].id.clone(); + let launch: ide_dap::LaunchConfig=serde_json::from_value(json!({"name":"Fake target","adapter":"debugpy","request":"launch","program":root.join("main.py"),"args":[],"cwd":root,"env":{},"extraJson":json!({"__runTarget":id}).to_string()})).unwrap(); + host.debugger + .set_breakpoints( + root.join("main.py"), + vec![ide_dap::Breakpoint { + line: 2, + enabled: true, + condition: None, + hit_condition: None, + log_message: None, + }], + ) + .await + .unwrap(); + let session = ide_host::ops::debug::debug_start(&host, "main", launch.clone()).await.unwrap(); + let frames = host.debugger.stack_trace(session, 1).await.unwrap(); + assert_eq!(frames[0].path.as_ref().unwrap().to_path_buf(), root.join("main.py")); + host.debugger.stop(session).await.unwrap(); + // External debug sources use the container reader even with no LSP running. + let library = base.join("library.py"); + std::fs::write(&library, "container library").unwrap(); + let uri = format!( + "openidea-target://{}/0/{}", + percent_encoding::utf8_percent_encode(&id, percent_encoding::NON_ALPHANUMERIC), + library.to_string_lossy().trim_start_matches('/') + ); + let doc = ide_host::ops::doc::doc_open_virtual(&host, "main", uri).await.unwrap(); + assert!(doc.read_only); + assert_eq!(doc.text, "container library"); + + std::fs::write(&wrapper, "#!/bin/sh\nexit 99\n").unwrap(); + let error = ide_host::ops::debug::debug_start(&host, "main", launch).await.unwrap_err(); + assert!(error.message.contains("changed") || error.message.contains("approved"), "{error:?}"); + host.close_project("main"); +} diff --git a/crates/ide-language/src/lib.rs b/crates/ide-language/src/lib.rs index fb3ff0807..ec390e25d 100644 --- a/crates/ide-language/src/lib.rs +++ b/crates/ide-language/src/lib.rs @@ -197,6 +197,9 @@ pub struct ServerTarget { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct DebuggerConfig { + /// Placement in plugin run targets; absent means unsupported. + #[serde(default)] + pub target_placement: Option, /// DAP adapter name, e.g. `codelldb`, `debugpy`, `js-debug`, `delve`. pub adapter: String, pub command: String, diff --git a/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources index e21331217..75010fdb6 100644 --- a/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources @@ -1 +1 @@ -446ff7b14c2d5b0fca8a3febe489906e9d56fc5b66878c953887e16dd54f5d5a +4647fa9aec9732e3dd824efccd61b1c6b9c36645a3586f402f6a7a47419db61f diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index ad3169e34..a24dbad77 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -c55acafff3dbe090aac50873d957e73add4db50b4ed3af0def7ec77c1cd1a9d5 +6787b195df1057176ea9fcf11d0fcaf8cd347fc182fab94fe3a229063deed831 diff --git a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources index c98371c10..b0c063aff 100644 --- a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -543e921a71667b481e9d7d3b6cf86bc46113ac7068a902d93063eff85c2c812d +c2d17076337b1225ef8b0323b335a3b03bf022485088957c520e0cb797d7df7d diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources index 52f9a6ac7..745fa2d97 100644 --- a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm.sources @@ -1 +1 @@ -fa9ccf5c4f999031e1fb94b9e42b6caddfd5e5aeb08181660f51488711326550 +415a4cd17c573aeeb1b45d4b1af55834eab5e686c4f624495323e4fb63c3ac71 diff --git a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources index 0fb5db03c..4ae8aef3d 100644 --- a/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm.sources @@ -1 +1 @@ -69fca6e054ccf188b7cfe23081470676bfbee56f96b0247c7ff67aba93a24d39 +5f502b7650acd5d07bffba8016680248b5ceac0926013916ff351562e3c6d831 diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources index 3da3fc1ef..fc61faa67 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources @@ -1 +1 @@ -cc2fdbb7953bac992375701812e8c4cf666652e027e8b6ecc68b43e0ae7d7ae1 +19e8d460269999aeac7b4021bfedb803ad0d68c24ce1c22f058070fb406a64c3 diff --git a/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm.sources index 7c56338bd..b0a3bfa73 100644 --- a/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm.sources @@ -1 +1 @@ -4e146d6caa1cc114d33ae18f396ac8da4d6391477daa32cc69e6feca087cf20a +63809abb0f8a4f40b0c0f465062cc8ba0ebfd03d41a108ff19866e6565eda931 diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources index 1ef247c14..e65bfe3e6 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources @@ -1 +1 @@ -cfb6e388588d01fd3f772302049e673729138717ce7b761dfcecc4ee213a8d98 +99c968b98d8f1d895c428b4048451275bd322e7070aa0a020737097efea17eb1 diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources index 256966493..6217ed1c2 100644 --- a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources @@ -1 +1 @@ -6117ebfa5220ab04f03884d589140426d2ee693a23f058e2457952f30314099d +c40a0fd8c6cf791e70ba0335c6cf0b2c82b250670ebad59b185e3e8719172409 diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources index 651736f72..d9150f1a6 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources @@ -1 +1 @@ -e746d420d5f357e00b523e6e118d283df08e7b5f09f59b672645721a02f6ca46 +162774155d8b49d53b0c0c055736bd098723c66b02c0e1dc4389985982c57282 diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 88330732d..70ae6f136 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -e127b6562e398cdb2d9d748434315ae78e61cfdfa3f1859da2e6eaeb2144030d +39e3de11f69c09348906878367318157fca9a777f9ac1c4fb6ed303fa058e5a2 diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index 4ac605dc8..3ddf4be2a 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -6459f7899464237399e33f4190a7526f05a010d6384a36f42c25195202fb6c58 +4a3f595d2a6a9b79da7e316a3da86c287e9761ea670319c331467e7b10639663 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 9d17287b1..dcd46bcda 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -15236a23798998a0e1669c0ce657effb88481ef7236f5738519601e23b07dd1f +bede1f0eaec356e3e35302e65e85531870625560bc9a3197f1acb58969beb4bd diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index 96e365a8e..6887c25d5 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -8af4f2658b074aebdabefbe49d0641da767c109b107ecf182bcb8cb937092b82 +ad2dc0ad12adb9c6b25017b2cef78b7496e96d1bb6e88b24ea9b7b682120e786 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index 1a1ba5036..535ef30b6 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -57a4527c74f62fb98ba388223863d69afdfc9f75b6027af65658f5909379e3c9 +2c329cf7af36c9d65ed5b6be7c2a017c7a5a6214a4893cab7ae574acb76fe62f diff --git a/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources index 492d5a0d7..7f06c4850 100644 --- a/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources @@ -1 +1 @@ -c2a29a1982c278a21a772e440662817b5f096b4740ac99539c5ab2721251acf8 +39c85942177e937e48f32bcde86a0068dac484ba028a8d6785a5a2728db9e11b diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 9e59d9c50..9d59fc312 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -634b3da9468c13d5a74a90d187f6c510b28abe4684e6072db630b976f331e899 +30a8cc05a40c7b99a78700430ae87d025ad7567b44384fdb696119aa421f8174 diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources index bb6cb7f95..5c9f9836b 100644 --- a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm.sources @@ -1 +1 @@ -b9d5c9b532e022bd4f43ecb80539efd6cab628ecba05c1ecb79d4ba677c1c8c0 +5f9c92b23c00269ec2b5dc8829ce526a8fc4c0d76ecb2925ec9a06ffeac5f0b4 diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources index 8800d454c..4739b4f92 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources @@ -1 +1 @@ -787ab4dbe4ef4ee8cd9af98079ae6332aeddacaa3f66fc1b3327f35e76f7ca01 +d695409029a66c8a44f524072be1d47ed974fc8810f6fe1d8d068765184d4028 diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index 7adc4fa8f..73bfc36a1 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -f0771c02543e5c96ae6a4f38402b666ef06d22abc1157dbc963c7fb2baee0221 +d5e81a43dc2c5298d0aaafd70c0bd91ab6cc040d70224d2936349de41253735f diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources index dbd2f0260..512389bf7 100644 --- a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources @@ -1 +1 @@ -86bd9d7f47e9dd9463bcef0173cd24bac4d83e9172adef3e620acc4abc6bc620 +c29076023ccedf2da8fd9e5a851a47bbe7131b97713c8140a20cd55608a1cc0d diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources index d796c2ba3..4a92be09f 100644 --- a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm.sources @@ -1 +1 @@ -d560db74cda33ff86e7a5329df8f6cd8675f3df0288e427325e0777778f03f44 +616157bd77dc272f9e2dfed4c54c9d65fca110e55ad01f92b7c4f8a6136537b4 diff --git a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources index a8c6a1aaf..4c26b5f91 100644 --- a/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm.sources @@ -1 +1 @@ -20b3cfd84d9129dd74b56c8766766fe714df7a5e6e051dc72c379d5fdb59b127 +022f216f0f4ca5da56f4b2fabf5ee4a987d5e9f9a752feecbb438f9b00b1acf4 diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources index 5307b0e6a..b0d33305d 100644 --- a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -1 +1 @@ -d30ffd74e9e444f51024586f9c67cefede44ad89bd251410ea3881bd2255b1d5 +626f0e0c83e69704c0365c273b6f63d43278ee37786f38ed467ce908c30eadf7 diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources index 1025ed3a0..5fa8950ef 100644 --- a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources @@ -1 +1 @@ -9a354ee52f1ee1080ce9c687e9fc59e137da8f38c6723316b8aeecb5bdf1e4ad +aa141701049f5c6c0d045341c3337344ed7a368800f08237b1b7812cce216814 diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources index 1a0ce43cb..43039f704 100644 --- a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm.sources @@ -1 +1 @@ -ae183781f1fc1639fa7b1b3c4b8ff100e4e25daab69bbf876584050e5363473f +d6c40099cc959ef2dead996beeb4acd73c1327597cd029674026c6f2bc878300 diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources index aa0be7b21..601107faf 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources @@ -1 +1 @@ -8a8c92661df8d693d5eabc45946e2d98c9e887090e4233e65e896421c655dcdd +3201f643cf868f7a358ba812f9c90e06b982d864ed141b6b4c3fdeee13cb1559 diff --git a/languages/php.toml b/languages/php.toml index 44ecf04d3..a6ed48287 100644 --- a/languages/php.toml +++ b/languages/php.toml @@ -38,6 +38,7 @@ require-root-marker = true install-hint = "composer global require laravel/lsp" [debugger] +target-placement = "connect-back" adapter = "php-debug" command = "php-debug-adapter" install-hint = "Install PHP Debug (Xdebug) in Settings › Languages & Frameworks › Language Servers, and enable Xdebug" diff --git a/languages/python.toml b/languages/python.toml index b3f331a44..8d94400e7 100644 --- a/languages/python.toml +++ b/languages/python.toml @@ -60,6 +60,7 @@ require-root-marker = true install-hint = "pip install ruff (or: uv tool install ruff)" [debugger] +target-placement = "adapter-in-target" adapter = "debugpy" command = "python3" args = ["-m", "debugpy.adapter"] diff --git a/plugins/README.md b/plugins/README.md index f9ecf9517..96182e1f8 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -2484,3 +2484,19 @@ The virtual URI identifies its originating server, including for document links and diagnostic-related locations. Navigation inside these documents returns to that server; editing and saving are disabled. No WIT signature change is needed for this opt-in host setting. + +### Debugging run targets + +Plugin targets require `container-root` for debugging. PHP uses `connect-back`: the +IDE runs php-debug on the host (on the SSH server for remote projects), listens on +port 9003 unless configured otherwise, then starts the program through the normal +target run path. The target must enable Xdebug debug mode and request startup, and +set `xdebug.client_host` and `xdebug.client_port` to reach that listener. DDEV normally +provides the host setting; enable its Xdebug support before debugging. + +Python uses `adapter-in-target`: the wrapper starts `python3 -m debugpy.adapter` +over stdio. Install debugpy in that container interpreter; no host installation is +used. The wrapper must preserve stdin/stdout and forward environment as needed. +Project paths and DAP sources map between the project root and `container-root`. +Other debuggers default to unsupported: Node and Go adapters currently require +additional ports and have no supported stdio target configuration. diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index 91979bbf6..01b21dc3c 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -34bea4bf36b7a814dafe02aaee02fe3354a667d1c8ca66f238de00322d0a035a +e3c11d8969a1b8b52e7e745b558d12f804ebf533e7f0f3fe614ffe6f7b9c9468 diff --git a/plugins/official/blame-annotations/dist/plugin.wasm.sources b/plugins/official/blame-annotations/dist/plugin.wasm.sources index 0f094100a..2273eeffd 100644 --- a/plugins/official/blame-annotations/dist/plugin.wasm.sources +++ b/plugins/official/blame-annotations/dist/plugin.wasm.sources @@ -1 +1 @@ -fdd68915997803652ad6f5c549107b043b9001ad4e176d2473d9b37469fbe1cc +b28ccfd442c6348e0826680ec97391cbe4ec6a93a27ff02e40f47341e21586b4 diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 091b95fcd..77c26c9be 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -ad3fe934d30b48d53097bf4dee36a091b2410c0325924702c66e717405b58203 +d669a2600b0c59e91ac8a9b875a24bddf0eeddd404ce7b67da31733861a846b8 diff --git a/plugins/official/container-runner/dist/plugin.wasm.sources b/plugins/official/container-runner/dist/plugin.wasm.sources index 651d6ae41..783e10d71 100644 --- a/plugins/official/container-runner/dist/plugin.wasm.sources +++ b/plugins/official/container-runner/dist/plugin.wasm.sources @@ -1 +1 @@ -5710601823307d167805a7ca20e7498fb9c37082b20df7fd1f86e1cd66b438f3 +a5f0ebce6d30e0dc03769ebe5cb82d3704c5ccc32207aa2b3f5500de7b1a9152 diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index 6d7a806f3..784a626bd 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -1a486ceda9986bc253a0098c983f755c8860e6b7a3f2d7f0284b3b928b4e6c9e +ee3ccb648ca4b98a0359521c4cd895b5e7336fd65101563f2ca82b8cbaeedc3d diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 702fcf51f..19fc760c6 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -13f27c819384f92e406bf7226a46d3fe5b063e850db3d64916e661e1a63f345a +51f7167dd5ed21ed37e227bd3b100a5be5a57e7890a494c942fdd036b1307993 diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 1f99a4425..6d2cdb427 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -c7b2b471f26201c037522ee4a5b086d1c8ef88fbbb3f11f7279ad1a7185bdbae +cd860e7a652951aaa42029da23fdeee7a3c09ab1bfcbfb0d1bb6c89036fc8817 diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 90ab3f4dd..19b7b5604 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -82c71af4a1f771dbd16a4c8f71b9bd6a6a53d82bd92958d4862467237b82d228 +db3ee94b5cf4234cee7d61e08a9f8629d7e7f6adbf11208b54cd4ea44cf1fc13 diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index ea3f9e602..32875ff17 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -65096a9b6331af9d40049801547e99e2fa1d5ae0f1065f6931af0aee8d2f24a7 +76d13e772046b192485ea59c7720177b563986e0167475c449a9a5c7430ef908 diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index bc35fb9ec..c8d4649db 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -cf8d21da412fd64f1f42c72b13be762ff1bcd43dd19965fc1fce805c5e1012ae +a22c9e7ebf79b2d5926424a028edc09aef40293db9ebc959d9ececa1f0ebe4ca diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 73dc03c73..afb2fb7a6 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -1ea1d7116e6b943e42580e43cb13199b6e48fc5acdf9784c4864adb7433ebb59 +fcc3d666fd1e89363fa8668dd10f89efc49f2376766282b734de4b719fdc9aff diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 417492708..5a18d3887 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -05ff46a6c1af09b84f9a931d8956d79fbad9507dcff708ba021e074c55c062e2 +2eb742315f877e3251f992884160ed4d9a2940ee12a7d4d7a52669238d495930 diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 3c1bb4b65..3c0fba9b4 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -26e57f5294b40bc31d9283843b2fcf6ad09d475fd160d61ddd3d420e54880a24 +c2feb6a4cd0e9908f078a3d0074732cfce653cc57cce9d1907838598515048ee diff --git a/plugins/official/test-runner/dist/plugin.wasm.sources b/plugins/official/test-runner/dist/plugin.wasm.sources index f54affe63..0e681748d 100644 --- a/plugins/official/test-runner/dist/plugin.wasm.sources +++ b/plugins/official/test-runner/dist/plugin.wasm.sources @@ -1 +1 @@ -0d2c1140fe718d06e08af903ee94bfe87cd04de77c1299f6f5c337042c6f6197 +590b46034f6c39592a27cabd5b0b76809e7f7133d258f322604614ecb3479317 diff --git a/plugins/official/todo-finder/dist/plugin.wasm.sources b/plugins/official/todo-finder/dist/plugin.wasm.sources index 86965003f..9b090f0ca 100644 --- a/plugins/official/todo-finder/dist/plugin.wasm.sources +++ b/plugins/official/todo-finder/dist/plugin.wasm.sources @@ -1 +1 @@ -f4251a1cad1d5cea54559baca12d84fdf175e65db85ee3f4d29e381e045c40fb +0d068ea0f0851e6e3166af0a91f290da8dc3f4a852b86050e7f7ced214a39124 diff --git a/sdk/rust/openidea-plugin/wit/run-targets.wit b/sdk/rust/openidea-plugin/wit/run-targets.wit index 66a41e700..ee436a9a5 100644 --- a/sdk/rust/openidea-plugin/wit/run-targets.wit +++ b/sdk/rust/openidea-plugin/wit/run-targets.wit @@ -16,7 +16,10 @@ interface run-targets { /// Without workdir-args the wrapper manages its container directory itself. /// The wrapper always starts in the configuration's LOCAL working directory. /// Environment reaches the wrapper; forwarding into a container depends on its conventions. - /// Registration never selects the target. Debugging plugin targets is invalid. + /// Registration never selects the target. Debugging requires container-root and an + /// explicitly supported language debugger placement. Connect-back adapters listen + /// on the host; the target must configure the debuggee to connect to that host. + /// Stdio adapters run inside the target and must be installed there. record run-target { id: string, name: string, diff --git a/src/features/debugger/actions.ts b/src/features/debugger/actions.ts index 75acdf27a..96e3c1487 100644 --- a/src/features/debugger/actions.ts +++ b/src/features/debugger/actions.ts @@ -1,4 +1,5 @@ /** Debugger actions (ids match `keymaps/intellij-macos.ts`). */ +import { withRunTarget } from "@/platform/runTargetSelection"; import { executeAction, getAction, registerActions, type Action } from "@/platform/actions"; import { call, @@ -315,11 +316,12 @@ async function startConfig(arg: unknown) { return; } } - if (isRemoteConfig(config)) { + config = withRunTarget(config); + if (!config.target?.startsWith("plugin-target:") && isRemoteConfig(config)) { await startRemote(config, root, show); return; } - if (isJavaDebuggable(config)) { + if (!config.target?.startsWith("plugin-target:") && isJavaDebuggable(config)) { await startJavaConfig(config, root, origin ?? config, show); return; } @@ -339,11 +341,21 @@ async function startConfig(arg: unknown) { }); return; } - const xdebugCheck = config.debugAdapter === "php-debug" && isTauri() ? warnWithoutXdebug(launch) : null; + const pluginTarget = config.target?.startsWith("plugin-target:"); + if (pluginTarget) { + const extra = JSON.parse(launch.extraJson ?? "{}"); + extra.__runTarget = config.target; + launch.extraJson = JSON.stringify(extra); + } + const xdebugCheck = + !pluginTarget && config.debugAdapter === "php-debug" && isTauri() ? warnWithoutXdebug(launch) : null; await maybeEditorService() ?.saveAll() .catch(() => undefined); - await startSession(launch, { runConfig: origin ?? config, show }); + const started = await startSession(launch, { runConfig: origin ?? config, show }); + if (started && pluginTarget && config.debugAdapter === "php-debug") { + await executeAction("run.runConfig", config); + } await xdebugCheck; } diff --git a/src/features/run/index.ts b/src/features/run/index.ts index b7c771274..061b00271 100644 --- a/src/features/run/index.ts +++ b/src/features/run/index.ts @@ -2,6 +2,7 @@ * Run configurations, the title bar run widget, the Run tool window (console + * test runner) and run/debug/stop actions. */ +import { withRunTarget } from "@/platform/runTargetSelection"; import { registerEditorContribution } from "@/editor/api"; import { executeAction, executeActionOrThrow, getAction, registerActions } from "@/platform/actions"; import { getContext, setContext, useContextKeys } from "@/platform/context"; @@ -146,6 +147,7 @@ async function runSelected() { } async function debugConfig(config: RunConfig, chain: readonly string[] = [], show?: boolean) { + config = withRunTarget(config); if (config.kind === "compound") { if (!(await prepareLaunch(config, chain, show))) return; const children = compoundChildren(config, useRun.getState().configs); @@ -155,6 +157,10 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = [], sho if (remoteDebugBlocked(config)) return; if (!(await prepareLaunch(config, chain, show))) return; recordRun(config.id); + if (config.target?.startsWith("plugin-target:")) { + await executeAction("debug.startConfig", show === false ? { config, show: false } : config); + return; + } // Python on a Docker / Compose target: debugpy in the container, the debugger attaches. if (containerDebuggable(config) && getAction("debug.attach")) { await debugInContainer(config); diff --git a/src/features/run/remote.test.tsx b/src/features/run/remote.test.tsx index c87304481..774a52ef4 100644 --- a/src/features/run/remote.test.tsx +++ b/src/features/run/remote.test.tsx @@ -217,6 +217,14 @@ describe("remote debugging", () => { ); }); + it("dispatches plugin targets to the target-aware debugger", async () => { + const config = cfg("app", { target: "plugin-target:4:wrapper", debugAdapter: "debugpy" }); + useRun.setState({ configs: [config], loaded: true }); + await executeAction("run.debugConfig", config.id); + expect(startConfig).toHaveBeenCalledWith(config); + expect(remoteDebugBlocked(config)).toBe(false); + }); + describe("in containers", () => { const container: RunTargetValid = { ...DEFAULT_RUN_TARGET, kind: "docker", id: "py", name: "Python", image: "x" }; const attach = vi.fn(); diff --git a/src/features/run/remote.ts b/src/features/run/remote.ts index 1ceb48da4..303b49fbf 100644 --- a/src/features/run/remote.ts +++ b/src/features/run/remote.ts @@ -69,7 +69,8 @@ export function containerDebuggable( */ export function remoteDebugBlocked(config: RunConfig, targets: readonly RuntimeRunTarget[] = allRunTargets()): boolean { config = withRunTarget(config); - if (!config.target || containerDebuggable(config, targets)) return false; + if (!config.target || config.target.startsWith("plugin-target:") || containerDebuggable(config, targets)) + return false; const target = targetName(config, targets) ?? config.target; const container = targetOf(config, targets); notifications().notify({ diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 8ab02e483..2f584353e 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -620,5 +620,9 @@ "plugin run target changed; navigate again": "plugin run target changed; navigate again", "plugin host is unavailable": "plugin host is unavailable", "language server targets belong to project settings": "language server targets belong to project settings", - "library documents are always read-only": "library documents are always read-only" + "library documents are always read-only": "library documents are always read-only", + "Debugging a plugin target requires a container root": "Debugging a plugin target requires a container root", + "This debugger does not support plugin run targets": "This debugger does not support plugin run targets", + "invalid debug launch arguments": "invalid debug launch arguments", + "unknown debug adapter": "unknown debug adapter" } diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 93cb225df..84123aee1 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -6963,5 +6963,9 @@ "plugin run target changed; navigate again": "插件运行目标已更改;请重新导航", "plugin host is unavailable": "插件宿主不可用", "language server targets belong to project settings": "语言服务器目标属于项目设置", - "library documents are always read-only": "库文档始终为只读" + "library documents are always read-only": "库文档始终为只读", + "Debugging a plugin target requires a container root": "调试插件目标需要容器根目录", + "This debugger does not support plugin run targets": "此调试器不支持插件运行目标", + "invalid debug launch arguments": "调试启动参数无效", + "unknown debug adapter": "未知调试适配器" } diff --git a/wit/run-targets.wit b/wit/run-targets.wit index 66a41e700..ee436a9a5 100644 --- a/wit/run-targets.wit +++ b/wit/run-targets.wit @@ -16,7 +16,10 @@ interface run-targets { /// Without workdir-args the wrapper manages its container directory itself. /// The wrapper always starts in the configuration's LOCAL working directory. /// Environment reaches the wrapper; forwarding into a container depends on its conventions. - /// Registration never selects the target. Debugging plugin targets is invalid. + /// Registration never selects the target. Debugging requires container-root and an + /// explicitly supported language debugger placement. Connect-back adapters listen + /// on the host; the target must configure the debuggee to connect to that host. + /// Stdio adapters run inside the target and must be installed there. record run-target { id: string, name: string,