diff --git a/crates/ide-host/src/ops/run.rs b/crates/ide-host/src/ops/run.rs index 8798e52a..f172a4f3 100644 --- a/crates/ide-host/src/ops/run.rs +++ b/crates/ide-host/src/ops/run.rs @@ -62,13 +62,24 @@ fn detected_and_saved( all } +/// Saves the user configurations. They come from [`configs`], toolchains applied, so what +/// settings derived is dropped again: only the user's own choices reach `run.toml`. pub fn save_configs( host: &Host, window: &str, configs: &[RunConfig], removed_detected_ids: &[String], ) -> IdeResult<()> { - ide_run::save_user_configs_with_removals(&host.project(window)?.root, configs, removed_detected_ids) + let project = host.project(window)?; + let settings = project.settings.effective(); + let configs: Vec = configs + .iter() + .map(|c| match c.kind.as_str() { + "gradle" => c.clone(), + _ => ide_sdk::apply::without_derived(c, &project.root, &settings), + }) + .collect(); + ide_run::save_user_configs_with_removals(&project.root, &configs, removed_detected_ids) } pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit) -> IdeResult { diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 0f9a937b..a59dc8c4 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -769,6 +769,47 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings } } +/// `config` as it should be saved: without what [`apply_to_run_configs`] derives from +/// settings, so a configuration read from the run list, edited and saved keeps following +/// Languages & Frameworks instead of freezing today's toolchain (and the IDE's whole +/// `PATH`) into `run.toml`. A value counts as derived when applying settings to the +/// configuration without it produces it again: an environment entry (`PATH`, +/// `VIRTUAL_ENV`, `JAVA_HOME`, `GOROOT`, …) or an absolute command its bare name +/// resolves to (the project interpreter, a toolchain's `cargo` / `go`, Maven's `mvn`). +pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConfig { + let applied = |c: &RunConfig| { + let mut c = c.clone(); + apply_to_run_configs(std::slice::from_mut(&mut c), root, s); + c + }; + let mut out = config.clone(); + // Until nothing changes: some entries are derived only once others are gone + // (the virtualenv's `PATH` is added only without `VIRTUAL_ENV`). + while let Some(key) = out + .env + .iter() + .find(|(key, value)| { + let mut probe = out.clone(); + probe.env.remove(*key); + applied(&probe).env.get(*key) == Some(*value) + }) + .map(|(key, _)| key.clone()) + { + out.env.remove(&key); + } + let path = Path::new(&config.command); + if path.is_absolute() + && let Some(name) = path.file_name().map(|n| n.to_string_lossy().into_owned()) + { + let mut probe = out.clone(); + probe.command = name.clone(); + if applied(&probe).command == config.command { + out.command = name; + } + } + out +} + fn sh_quote(s: &str) -> String { format!("'{}'", s.replace('\'', r"'\''")) } diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index e777b10b..2385a404 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -1,6 +1,6 @@ use crate::apply::{ apply_to_run_configs, configure_server, jdtls_jdk, maven_command, maven_options, project_jdk, python_interpreter, - terminal_activation, tsdk, + terminal_activation, tsdk, without_derived, }; use crate::python::parse_pip_list; use crate::{bin_dir, exe, venv_of}; @@ -1108,3 +1108,84 @@ fn go_modules_configure_gopls_in_place_and_commands_with_explicit_run_overrides( configure_server(&mut cleared, tmp.path(), tmp.path(), &Settings::default()); assert_eq!(cleared.settings.unwrap()["env"], json!({})); } + +#[test] +fn saving_a_loaded_run_configuration_drops_what_settings_derived() { + if cfg!(windows) { + return; // Unix paths + } + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let touch = |rel: &str| { + let p = root.join(rel); + std::fs::create_dir_all(p.parent().unwrap()).unwrap(); + std::fs::write(&p, "").unwrap(); + p.to_string_lossy().into_owned() + }; + let mut s = Settings::default(); + s.languages.python.interpreter = fake_venv(root, ".venv").to_string_lossy().into_owned(); + s.languages.node.interpreter = touch("node/bin/node"); + touch("rustup/toolchains/stable/bin/cargo"); + s.languages.rust.toolchain = root.join("rustup/toolchains/stable").to_string_lossy().into_owned(); + touch("go/bin/go"); + s.languages.go.goroot = root.join("go").to_string_lossy().into_owned(); + s.languages.go.gopath = root.join("gopath").to_string_lossy().into_owned(); + s.languages.go.modules.env = BTreeMap::from([("GOPROXY".into(), "https://proxy.example".into())]); + fake_jdk(&root.join("jdk17")); + s.languages.java.jdk = root.join("jdk17").to_string_lossy().into_owned(); + touch("maven/bin/mvn"); + s.languages.java.maven.home = root.join("maven").to_string_lossy().into_owned(); + s.languages.ruby.interpreter = touch("ruby/bin/ruby"); + touch("ruby/bin/bundle"); + s.tools.insert("godot".into(), ide_core::settings::ToolSettings { path: touch("Godot/godot") }); + + let saved = |kind: &str, command: &str, args: &[&str], env: &[(&str, &str)]| { + let mut c = run(kind, command, args); + c.detected = false; + c.env = env.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(); + c + }; + // As the user saved them: bare tools, only their own environment. + let configs = [ + saved("python", "python", &["app.py"], &[]), + saved("pytest", ".venv/bin/pytest", &["-x"], &[("FOO", "1")]), + saved("npm", "npm", &["run", "dev"], &[]), + saved("cargo", "cargo", &["run"], &[]), + saved("go", "go", &["run", "."], &[]), + saved("go", "go", &["mod", "tidy"], &[("GOPROXY", "off")]), + saved("maven", "mvn", &["package"], &[]), + saved("tomcat", "catalina", &[], &[]), + saved("ruby", "ruby", &["app.rb"], &[]), + saved("rake", "bundle", &["exec", "rake"], &[("RAILS_ENV", "test")]), + saved("godot", "godot", &["--path", "."], &[]), + // The user's own values that merely look like derived ones stay. + saved("cargo", "/opt/rust/bin/cargo", &["build"], &[("PATH", "/custom/bin")]), + saved("maven", "mvn", &["verify"], &[("JAVA_HOME", "/other/jdk")]), + saved("python", "python", &["x.py"], &[("VIRTUAL_ENV", "/elsewhere/venv")]), + ]; + for user in configs { + let mut loaded = user.clone(); + apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s); + assert_ne!(loaded, user, "{} {}: settings apply to it", user.kind, user.command); + let stored = without_derived(&loaded, root, &s); + assert_eq!(stored, user, "{} {}: saved as the user left it", user.kind, user.command); + let mut reloaded = stored; + apply_to_run_configs(std::slice::from_mut(&mut reloaded), root, &s); + assert_eq!(reloaded, loaded, "{} {}: runs the same after a save", user.kind, user.command); + } + + // Another bare name for the interpreter comes back as the interpreter's own. + let mut loaded = saved("python", "python3", &["app.py"], &[]); + apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s); + assert_eq!(without_derived(&loaded, root, &s).command, exe("python")); + + // A saved configuration follows a later interpreter change instead of a frozen PATH. + let mut loaded = saved("ruby", "ruby", &["app.rb"], &[]); + apply_to_run_configs(std::slice::from_mut(&mut loaded), root, &s); + let mut next = without_derived(&loaded, root, &s); + s.languages.ruby.interpreter = touch("ruby2/bin/ruby"); + apply_to_run_configs(std::slice::from_mut(&mut next), root, &s); + let ruby2 = root.join("ruby2/bin"); + assert_eq!(next.command, ruby2.join("ruby").to_string_lossy()); + assert!(next.env["PATH"].starts_with(&*ruby2.to_string_lossy()), "{}", next.env["PATH"]); +}