From 6c1eba9d82b43d335de6669d73f986713e282c0f Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:03:19 +0800 Subject: [PATCH] feat(gradle): add Gradle settings with Gradle JVM and distribution (#303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Settings › Build Tools › Gradle: Gradle JVM (defaults to the Project JDK), distribution (wrapper or local installation with its home) and Gradle user home, stored in the project or global settings. Sync and Gradle tool-window tasks use the configured JVM, launcher and GRADLE_USER_HOME. Run configurations keep their own choices: one that uses the wrapper runs gradlew, one that runs plain `gradle` resolves it from the local installation, then the pinned toolchain, then PATH; an explicit JAVA_HOME in the configuration wins over Gradle JVM. Derived defaults are applied at launch and not saved into run configurations. New Gradle configurations start with the wrapper off when the project uses a local installation. Fixes #232 --- crates/ide-build/src/sync.rs | 34 ++++++++++- crates/ide-core/src/settings.rs | 45 ++++++++++++++ crates/ide-host/src/ops/build.rs | 34 ++++++++++- crates/ide-host/src/ops/run.rs | 11 +++- crates/ide-host/tests/run_configs.rs | 26 ++++++++ crates/ide-sdk/src/apply.rs | 60 ++++++++++++++++--- crates/ide-sdk/src/tests.rs | 55 +++++++++++++++++ crates/ide-sdk/tests/version_managers.rs | 13 +++- src/bindings.ts | 15 +++++ src/features/run/runConfigTypes.test.tsx | 22 +++++++ src/features/run/types.ts | 13 +++- .../settings/__tests__/dialog.test.tsx | 22 +++++++ .../settings/languages/LanguagePages.tsx | 52 +++++++++++++++- src/features/settings/pages/execution.tsx | 27 ++++++++- src/platform/languageSettings.ts | 9 +++ src/platform/locales/text/zh.json | 15 ++++- 16 files changed, 434 insertions(+), 19 deletions(-) diff --git a/crates/ide-build/src/sync.rs b/crates/ide-build/src/sync.rs index 7529e2fb9..570195cc4 100644 --- a/crates/ide-build/src/sync.rs +++ b/crates/ide-build/src/sync.rs @@ -32,6 +32,8 @@ pub struct SyncOptions { pub env: BTreeMap, /// Maven launcher replacing the project's `mvnw` / `mvn` (Settings › Maven home). pub maven_command: Option, + /// Resolved Gradle launcher from Settings. + pub gradle_command: Option, /// Maven options before the goals (`-s`, `-Dmaven.repo.local=`). pub maven_options: Vec, } @@ -268,7 +270,14 @@ pub fn sync_gradle( args.push("--offline".into()); } progress(SyncProgress { message: "Loading Gradle tasks".into(), fraction: None }); - let out = run(&model.command, &args, &root.join(&model.dir), &options.env, cancel, |_| {})?; + let out = run( + options.gradle_command.as_deref().unwrap_or(&model.command), + &args, + &root.join(&model.dir), + &options.env, + cancel, + |_| {}, + )?; let cache = GradleCache { hash: hash_files(&gradle::build_files(root, &model)), at_ms: now_ms(), @@ -282,6 +291,29 @@ pub fn sync_gradle( mod tests { use super::*; + #[cfg(unix)] + #[test] + fn gradle_sync_uses_resolved_command_and_environment() { + use std::os::unix::fs::PermissionsExt; + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + std::fs::write(root.join("build.gradle"), "").unwrap(); + let launcher = root.join("configured-gradle"); + std::fs::write(&launcher, "#!/bin/sh\nprintf '%s\\n' \"$JAVA_HOME\" \"$GRADLE_USER_HOME\" > actual-env\n") + .unwrap(); + std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755)).unwrap(); + let options = SyncOptions { + gradle_command: Some(launcher.to_string_lossy().into_owned()), + env: BTreeMap::from([ + ("JAVA_HOME".into(), "/jdk21".into()), + ("GRADLE_USER_HOME".into(), "/gradle-cache".into()), + ]), + ..Default::default() + }; + sync_gradle(root, &root.join("cache"), &options, &CancellationToken::new(), &|_| {}).unwrap(); + assert_eq!(std::fs::read_to_string(root.join("actual-env")).unwrap(), "/jdk21\n/gradle-cache\n"); + } + #[test] fn caches_merge_and_go_stale() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 541ca6f95..0a6ff2ec0 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -563,6 +563,29 @@ pub struct JavaSettings { pub jdtls_vm_args: String, /// Build Tools › Maven. pub maven: MavenSettings, + /// Build Tools › Gradle. + pub gradle: GradleSettings, +} + +/// Build Tools › Gradle. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] +#[serde(default, rename_all = "camelCase")] +pub struct GradleSettings { + /// Empty = Project JDK; otherwise a JDK home. + pub jvm: String, + pub distribution: GradleDistribution, + /// Used only for the local distribution. + pub home: String, + /// Empty = Gradle's default user home. + pub user_home: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub enum GradleDistribution { + #[default] + Wrapper, + Local, } /// Build Tools › Maven. @@ -709,3 +732,25 @@ mod maven_linkage_tests { assert_eq!(serde_json::from_str::(&encoded).unwrap(), settings); } } + +#[cfg(test)] +mod gradle_settings_tests { + use super::*; + + #[test] + fn defaults_and_round_trip() { + let old: JavaSettings = serde_json::from_str(r#"{"jdk":"/jdk"}"#).unwrap(); + assert_eq!(old.gradle, GradleSettings::default()); + for distribution in [GradleDistribution::Wrapper, GradleDistribution::Local] { + let mut settings = Settings::default(); + settings.languages.java.gradle = GradleSettings { + jvm: "/jdk21".into(), + distribution, + home: "/gradle".into(), + user_home: "/cache/gradle".into(), + }; + let restored: Settings = serde_json::from_str(&serde_json::to_string(&settings).unwrap()).unwrap(); + assert_eq!(restored.languages.java.gradle, settings.languages.java.gradle); + } + } +} diff --git a/crates/ide-host/src/ops/build.rs b/crates/ide-host/src/ops/build.rs index c60d880a3..2ee567f32 100644 --- a/crates/ide-host/src/ops/build.rs +++ b/crates/ide-host/src/ops/build.rs @@ -46,7 +46,8 @@ pub fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel { } } if let Some(g) = &mut model.gradle { - g.env = java_env(jdk.as_ref(), g.java_version); + g.env = ide_sdk::apply::gradle_env(root, settings, java_env(jdk.as_ref(), g.java_version)); + g.command = ide_sdk::apply::gradle_command(root, &root.join(&g.dir), settings).to_string_lossy().into_owned(); } // A failed sync of a project no installed JDK can build says so first (with a Download JDK action). let missing = |release: Option| match jdk { @@ -67,7 +68,7 @@ pub fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel { if let Some(g) = &mut model.gradle && g.sync_error.is_some() { - let hint = missing(g.java_version); + let hint = if settings.languages.java.gradle.jvm.trim().is_empty() { missing(g.java_version) } else { None }; explain(&mut g.sync_error, hint); } model @@ -113,6 +114,7 @@ pub async fn build_sync( maven_command: ide_sdk::apply::maven_command(&root, &settings).map(|p| p.to_string_lossy().into_owned()), maven_options: ide_sdk::apply::maven_options(&root, &settings), maven_settings: settings.languages.java.maven.clone(), + gradle_command: current.gradle.as_ref().map(|g| g.command.clone()), }; let progress = |p: SyncProgress| { let _ = on_progress.send(p); @@ -267,3 +269,31 @@ pub(crate) fn register(d: &mut Dispatcher) { .await }); } + +#[cfg(test)] +mod gradle_settings_tests { + use super::*; + + #[test] + fn model_for_sync_tasks_and_compile_uses_gradle_settings() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + std::fs::write(root.join("build.gradle"), "plugins { id 'java' }").unwrap(); + let wrapper = root.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" }); + std::fs::write(&wrapper, "").unwrap(); + let mut settings = Settings::default(); + let default = load(root, &root.join("cache"), &settings).gradle.unwrap(); + assert_eq!(default.command, wrapper.to_string_lossy()); + settings.languages.java.gradle.jvm = "jdk21".into(); + settings.languages.java.gradle.user_home = "user-home".into(); + settings.languages.java.gradle.distribution = ide_core::settings::GradleDistribution::Local; + settings.languages.java.gradle.home = "gradle-install".into(); + let model = load(root, &root.join("cache"), &settings).gradle.unwrap(); + assert_eq!( + model.command, + root.join("gradle-install/bin").join(if cfg!(windows) { "gradle.bat" } else { "gradle" }).to_string_lossy() + ); + assert_eq!(model.env["JAVA_HOME"], root.join("jdk21").to_string_lossy()); + assert_eq!(model.env["GRADLE_USER_HOME"], root.join("user-home").to_string_lossy()); + } +} diff --git a/crates/ide-host/src/ops/run.rs b/crates/ide-host/src/ops/run.rs index aaf1fdd39..3dfe64fed 100644 --- a/crates/ide-host/src/ops/run.rs +++ b/crates/ide-host/src/ops/run.rs @@ -18,7 +18,10 @@ pub fn configs(host: &Host, window: &str) -> IdeResult> { .collect(); all.extend(user); // Project interpreter / package manager / toolchains from Languages & Frameworks. - ide_sdk::apply::apply_to_run_configs(&mut all, &root, &settings); + // Keep Gradle choices unmodified in the editor: derived defaults must not be saved as explicit env. + for config in all.iter_mut().filter(|c| c.kind != "gradle") { + ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(config), &root, &settings); + } Ok(all) } @@ -48,7 +51,11 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit let settings = project.settings.effective(); // Configurations made in the UI name bare tools (`godot`) that may live outside PATH. let mut config = config; - ide_sdk::apply::resolve_run_command(&mut config, &project.root, &settings); + if config.kind == "gradle" { + ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(&mut config), &project.root, &settings); + } else { + ide_sdk::apply::resolve_run_command(&mut config, &project.root, &settings); + } if config.command == ide_sdk::godot::COMMAND { return Err(IdeError::unavailable(ide_sdk::godot::NOT_FOUND)); } diff --git a/crates/ide-host/tests/run_configs.rs b/crates/ide-host/tests/run_configs.rs index 5485d2127..4008b0e33 100644 --- a/crates/ide-host/tests/run_configs.rs +++ b/crates/ide-host/tests/run_configs.rs @@ -51,3 +51,29 @@ async fn removed_detections_stay_removed_and_overrides_restore() { ops::run::save_configs(&host, "main", &[], &[]).unwrap(); assert!(!ops::run::configs(&host, "main").unwrap().iter().any(|c| c.id == id)); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn gradle_defaults_are_not_saved_as_explicit_run_choices() { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); + std::fs::write(root.join("build.gradle"), "plugins { id 'java' }").unwrap(); + let host = host(&root.join("app")); + host.open_project("main", &root).unwrap(); + ops::project::set_settings( + &host, + "main", + ide_workspace::settings::SettingsScope::Project, + "languages.java.gradle.jvm", + "\"/configured-jdk\"", + ) + .unwrap(); + let configs = ops::run::configs(&host, "main").unwrap(); + let mut gradle = configs.into_iter().find(|c| c.kind == "gradle").unwrap(); + assert!(!gradle.env.contains_key("JAVA_HOME")); + gradle.detected = false; + gradle.env.insert("JAVA_HOME".into(), "/explicit-jdk".into()); + ops::run::save_configs(&host, "main", &[gradle.clone()], &[]).unwrap(); + let saved = ops::run::configs(&host, "main").unwrap().into_iter().find(|c| c.id == gradle.id).unwrap(); + assert_eq!(saved.env["JAVA_HOME"], "/explicit-jdk"); + assert_eq!(saved.command, gradle.command); +} diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 1e61c27ed..f89177ff6 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -116,6 +116,43 @@ pub fn maven_command(root: &Path, s: &Settings) -> Option { found } +/// Project launcher policy for sync and tool-window tasks. +pub fn gradle_command(root: &Path, build_dir: &Path, s: &Settings) -> PathBuf { + resolve_gradle_command(root, build_dir, s, true) +} + +fn resolve_gradle_command(root: &Path, build_dir: &Path, s: &Settings, use_wrapper: bool) -> PathBuf { + let g = &s.languages.java.gradle; + let name = if cfg!(windows) { "gradle.bat" } else { "gradle" }; + if g.distribution == ide_core::settings::GradleDistribution::Local + && let Some(home) = setting_path(root, &g.home) + { + return home.join("bin").join(name); + } + let wrapper = build_dir.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" }); + if use_wrapper && wrapper.is_file() { + return wrapper; + } + pinned::pinned_install(build_dir, Tool::Gradle) + .map(|(home, _)| home.join("bin").join(name)) + .unwrap_or_else(|| name.into()) +} + +/// Gradle JVM overrides Project JDK; model callers supply the module's fallback environment. +pub fn gradle_env( + root: &Path, + s: &Settings, + mut env: std::collections::BTreeMap, +) -> std::collections::BTreeMap { + if let Some(home) = setting_path(root, &s.languages.java.gradle.jvm) { + env.insert("JAVA_HOME".into(), home.to_string_lossy().into_owned()); + } + if let Some(home) = setting_path(root, &s.languages.java.gradle.user_home) { + env.insert("GRADLE_USER_HOME".into(), home.to_string_lossy().into_owned()); + } + env +} + /// Maven options from settings, placed before the goals: `-s ` /// and `-Dmaven.repo.local=`. pub fn maven_options(root: &Path, s: &Settings) -> Vec { @@ -532,7 +569,6 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings let jdk = project_jdk(root, s); let mvn = maven_command(root, s); let mvn_options = maven_options(root, s); - let gradle = pinned::pinned_install(root, Tool::Gradle).map(|(dir, _)| dir.join("bin")); for c in configs.iter_mut() { resolve_run_command(c, root, s); @@ -600,15 +636,25 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings c.env.entry("GOPATH".into()).or_insert_with(|| gopath.to_string_lossy().into_owned()); } } - "maven" | "gradle" => { + "gradle" => { + let build_dir = c.cwd.as_ref().map(|p| root.join(p.as_ref())).unwrap_or_else(|| root.to_path_buf()); + if matches!(c.command.as_str(), "gradle" | "gradle.bat") { + c.command = resolve_gradle_command(root, &build_dir, s, false).to_string_lossy().into_owned(); + } + let defaults = jdk + .as_ref() + .map(|j| { + std::collections::BTreeMap::from([("JAVA_HOME".into(), j.home.to_string_lossy().into_owned())]) + }) + .unwrap_or_default(); + for (key, value) in gradle_env(root, s, defaults) { + c.env.entry(key).or_insert(value); + } + } + "maven" => { if let Some(jdk) = &jdk { c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned()); } - if c.kind == "gradle" - && let Some(bin) = gradle.as_ref().filter(|b| b.is_dir()) - { - path_dirs.push(bin.clone()); - } if c.kind == "maven" && c.detected { if let Some(mvn) = &mvn { c.command = mvn.to_string_lossy().into_owned(); diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index 9dee3aff8..eede589e6 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -698,3 +698,58 @@ fn max_java_picks_an_older_jdk() { crate::java::cap_java_home(&mut free, &jdks); assert_eq!(free.env["JAVA_HOME"], j25.home.to_string_lossy()); } + +#[test] +fn gradle_settings_precedence() { + use crate::apply::{gradle_command, gradle_env}; + use ide_core::settings::GradleDistribution; + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let plain = if cfg!(windows) { "gradle.bat" } else { "gradle" }; + let wrapper = root.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" }); + let mut settings = Settings::default(); + assert_eq!(gradle_command(root, root, &settings), std::path::PathBuf::from(plain)); + std::fs::write(&wrapper, "").unwrap(); + assert_eq!(gradle_command(root, root, &settings), wrapper); + let fallback = std::collections::BTreeMap::from([("JAVA_HOME".into(), "project-jdk".into())]); + assert_eq!(gradle_env(root, &settings, fallback.clone()), fallback); + settings.languages.java.gradle.jvm = "gradle-jdk".into(); + settings.languages.java.gradle.user_home = "gradle-cache".into(); + settings.languages.java.gradle.home = "local-gradle".into(); + let env = gradle_env(root, &settings, fallback); + assert_eq!(env["JAVA_HOME"], root.join("gradle-jdk").to_string_lossy()); + assert_eq!(env["GRADLE_USER_HOME"], root.join("gradle-cache").to_string_lossy()); + // A stored local home is inactive in wrapper mode. + assert_eq!(gradle_command(root, root, &settings), wrapper); + for local in [false, true] { + settings.languages.java.gradle.distribution = + if local { GradleDistribution::Local } else { GradleDistribution::Wrapper }; + let expected = if local { root.join("local-gradle/bin").join(plain) } else { plain.into() }; + for explicit_java in [false, true] { + for is_test in [false, true] { + let mut configs = + [run("gradle", wrapper.to_str().unwrap(), &["test"]), run("gradle", plain, &["test"])]; + for c in &mut configs { + c.is_test = is_test; + if explicit_java { + c.env.insert("JAVA_HOME".into(), "explicit-jdk".into()); + } + } + apply_to_run_configs(&mut configs, root, &settings); + assert_eq!(configs[0].command, wrapper.to_string_lossy()); + assert_eq!(configs[1].command, expected.to_string_lossy()); + for c in &configs { + assert_eq!( + c.env["JAVA_HOME"], + if explicit_java { + "explicit-jdk".into() + } else { + root.join("gradle-jdk").to_string_lossy().into_owned() + } + ); + } + } + } + assert_eq!(gradle_command(root, root, &settings), if local { expected } else { wrapper.clone() }); + } +} diff --git a/crates/ide-sdk/tests/version_managers.rs b/crates/ide-sdk/tests/version_managers.rs index 6131f14e5..4fbd3bd51 100644 --- a/crates/ide-sdk/tests/version_managers.rs +++ b/crates/ide-sdk/tests/version_managers.rs @@ -116,10 +116,21 @@ fn mise_and_asdf_installs_are_detected_and_pinned_versions_used() { assert_eq!(path_of(&configs[0])[0], installs.join("node/20.11.1/bin")); assert_eq!(configs[1].command, goroot.join("bin/go").to_string_lossy()); assert_eq!(configs[1].env["GOROOT"], goroot.to_string_lossy()); - assert_eq!(path_of(&configs[2])[0], installs.join("gradle/8.7/bin")); + assert_eq!(configs[2].command, installs.join("gradle/8.7/bin/gradle").to_string_lossy()); assert_eq!(configs[2].env["JAVA_HOME"], jdk.home.to_string_lossy()); assert_eq!(configs[3].command, maven.join("bin/mvn").to_string_lossy()); + assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &s), installs.join("gradle/8.7/bin/gradle")); + script(&project.join("gradlew"), ""); + assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &s), project.join("gradlew")); + let mut local = s.clone(); + local.languages.java.gradle.distribution = ide_core::settings::GradleDistribution::Local; + local.languages.java.gradle.home = "local-gradle".into(); + assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &local), project.join("local-gradle/bin/gradle")); + let mut plain = [run("gradle", "gradle")]; + ide_sdk::apply::apply_to_run_configs(&mut plain, &project, &s); + assert_eq!(plain[0].command, installs.join("gradle/8.7/bin/gradle").to_string_lossy()); + // The project's Maven wrapper keeps precedence over a pinned Maven. script(&project.join("mvnw"), ""); assert_eq!(ide_sdk::apply::maven_command(&project, &s), None); diff --git a/src/bindings.ts b/src/bindings.ts index 6f56a1da6..224083722 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -3139,6 +3139,8 @@ export type GoSettings = { export type GotoKind = "definition" | "declaration" | "typeDefinition" | "implementation"; +export type GradleDistribution = "wrapper" | "local"; + export type GradleModel = { /** Project-relative directory of the build (where `settings.gradle` is); `""` = project root. */ dir: string, @@ -3166,6 +3168,17 @@ export type GradleProject = { tasks: GradleTask[], }; +/** Build Tools › Gradle. */ +export type GradleSettings = { + /** Empty = Project JDK; otherwise a JDK home. */ + jvm?: string, + distribution?: GradleDistribution, + /** Used only for the local distribution. */ + home?: string, + /** Empty = Gradle's default user home. */ + userHome?: string, +}; + export type GradleTask = { name: string, /** Lower-case group (`build`, `build setup`, `other`). */ @@ -3614,6 +3627,8 @@ export type JavaSettings = { jdtlsVmArgs?: string, /** Build Tools › Maven. */ maven?: MavenSettings, + /** Build Tools › Gradle. */ + gradle?: GradleSettings, }; /** Java versions a vendor publishes. */ diff --git a/src/features/run/runConfigTypes.test.tsx b/src/features/run/runConfigTypes.test.tsx index 168a6e1d8..1c0b0b791 100644 --- a/src/features/run/runConfigTypes.test.tsx +++ b/src/features/run/runConfigTypes.test.tsx @@ -1,3 +1,4 @@ +import { useProject } from "@/platform/project"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import { registerActions } from "@/platform/actions"; @@ -22,6 +23,7 @@ import { pasteRows } from "./EnvDialog"; import { CURRENT_FILE, resetRuns, selectConfig, sessionsOf, startCompound, startRun, useRun } from "./store"; import { BUILTIN_TYPES, + blankTemplate, formParams, missingParams, newConfigOfType, @@ -519,3 +521,23 @@ describe("Current File", () => { expect(useRun.getState().selectedId).toBe(CURRENT_FILE); }); }); + +describe("new Gradle wrapper default", () => { + it.each(["wrapper", "local"] as const)( + "follows %s distribution without changing existing choices", + (distribution) => { + const previous = useProject.getState().settings; + useProject.setState({ settings: { languages: { java: { gradle: { distribution } } } } as never }); + try { + const type = BUILTIN_TYPES.find((t) => t.id === "gradle")!; + const config = newConfigOfType(type, [], blankTemplate(type)); + expect(config.params?.wrapper).toBe(distribution === "local" ? "false" : "true"); + expect(type.fields.some((f) => f.key === "wrapper")).toBe(true); + const template = { ...config, params: { wrapper: "true" } }; + expect(newConfigOfType(type, [], template).params?.wrapper).toBe("true"); + } finally { + useProject.setState({ settings: previous }); + } + }, + ); +}); diff --git a/src/features/run/types.ts b/src/features/run/types.ts index 95c499db8..c95952d6a 100644 --- a/src/features/run/types.ts +++ b/src/features/run/types.ts @@ -16,6 +16,7 @@ import { type ForkedJvm, } from "./jvmTypes"; import { tx } from "@/platform/i18n"; +import { useProject } from "@/platform/project"; export const COMPOUND = "compound"; @@ -501,11 +502,19 @@ export function blankTemplate(type: RunConfigTypeContribution): RunConfig { isTest: false, debugAdapter: type.debugAdapter ?? null, }, - { ...type.defaults }, + newConfigDefaults(type), type, ); } +function newConfigDefaults(type: RunConfigTypeContribution): Record { + const defaults = { ...type.defaults }; + if (type.id === "gradle" && useProject.getState().settings?.languages?.java?.gradle?.distribution === "local") { + defaults.wrapper = "false"; + } + return defaults; +} + /** A new configuration of `type`, with the type's defaults and then its template applied. */ export function newConfigOfType( type: RunConfigTypeContribution, @@ -534,7 +543,7 @@ export function newConfigOfType( if (v !== undefined) (copied as Record)[key] = structuredClone(v); } } - const params = { ...type.defaults, ...(template?.params ?? {}) }; + const params = { ...newConfigDefaults(type), ...(template?.params ?? {}) }; const next = withParams({ ...base, ...copied }, params, type); return type.debugAdapter ? { ...next, debugAdapter: type.debugAdapter } : next; } diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx index 63857e202..0ab46c953 100644 --- a/src/features/settings/__tests__/dialog.test.tsx +++ b/src/features/settings/__tests__/dialog.test.tsx @@ -65,6 +65,28 @@ async function openDialog(page?: string) { } describe("Settings dialog", () => { + it("finds Gradle JVM and applies project Gradle settings", async () => { + useProject.setState({ project: { root: "/project" } as never }); + const dialog = await openDialog("build.gradle"); + fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "Gradle JVM" } }); + expect(within(dialog).getByRole("tree", { name: "Settings pages" }).textContent).toContain("Gradle"); + expect(within(dialog).getByLabelText("Gradle JVM")).toBeInTheDocument(); + await act(async () => stage("build.gradle", "project", "languages.java.gradle.jvm", "/jdk21")); + await act(async () => stage("build.gradle", "project", "languages.java.gradle.distribution", "local")); + expect(within(dialog).getByPlaceholderText("Gradle installation directory")).toBeInTheDocument(); + await act(async () => stage("build.gradle", "project", "languages.java.gradle.home", "/gradle")); + await act(async () => stage("build.gradle", "project", "languages.java.gradle.userHome", "/gradle-cache")); + await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" }))); + expect(settingsSet.mock.calls).toEqual( + expect.arrayContaining([ + ["project", "languages.java.gradle.jvm", '"/jdk21"'], + ["project", "languages.java.gradle.distribution", '"local"'], + ["project", "languages.java.gradle.home", '"/gradle"'], + ["project", "languages.java.gradle.userHome", '"/gradle-cache"'], + ]), + ); + }); + it("searches pages and highlights matching option labels", async () => { const dialog = await openDialog("appearance"); fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "soft wrap" } }); diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx index 7ef5718d1..a106da639 100644 --- a/src/features/settings/languages/LanguagePages.tsx +++ b/src/features/settings/languages/LanguagePages.tsx @@ -266,7 +266,57 @@ export function JavaPage() { } // --------------------------------------------------------------------------- -// Build Tools › Maven +// Build Tools › Gradle / Maven + +export function GradlePage() { + const [distribution, setDistribution] = useSetting("languages.java.gradle.distribution"); + return ( +
+
+ + +