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 ( +
+
+ + +