feat(gradle): import modules from Gradle itself (includeFlat, included builds, computed includes) (#715)

The Gradle model came from a static parse of settings.gradle, which cannot follow includeFlat, projectDir assignments, computed include lists or included builds, so those modules were missing from the Gradle tool window, the Project view, indexing and run. Sync now runs an init script that prints every evaluated project (identity path, directory, build directory, source sets with test classification, Java version) without resolving dependencies; the static parser stays as the fallback before the first sync. Modules outside the root become content roots, the Project view lists the build's module group above the project folder, an unsynced Gradle build is imported once when opened, and staleness ignores comments and whitespace in build scripts.

Application / Spring Boot configurations of a Gradle module run through a JavaExec task on the source set's runtime classpath by default (Settings › Build Tools › Gradle › Build and run using), with the debugger attached to the forked JVM. The build model reaches run and the Project view through a platform store rather than a cross-feature import.

Fixes #737
Fixes #861

Co-authored-by: ntbang0901 <[email protected]>
Цей коміт міститься в:
ntbang0901 authored and GitHub committed 2026-11-04 01:09:07 +08:00
1 джерело 0c1a64bc2c
коміт bd424a3805
34 files changed
+1454 -110

No files matched your search

+16
Переглянути файл
@@ -0,0 +1,16 @@
/**
* The open project's build model as the Maven / Gradle tool windows last loaded it, for the
* features that follow the build (run, Project view) without loading it again.
*/
import { create } from "zustand";
import type { BuildModel } from "./ipc";
export interface SharedBuildModel {
/** Project the model belongs to. */
root: string | null;
model: BuildModel | null;
/** Gradle tool window's "Offline" toggle. */
gradleOffline: boolean;
}
export const useBuildModel = create<SharedBuildModel>(() => ({ root: null, model: null, gradleOffline: false }));
+13 -4
Переглянути файл
@@ -6,6 +6,7 @@
*/
import { call, commands, IpcError, type JavaLaunchInfo, type RunConfig, type RequestId } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { useBuildPresentation } from "@/platform/projectPresentation";
export const CLASSPATH = "${classpath}";
export const TEST_CLASSPATH = "${testClasspath}";
@@ -117,11 +118,19 @@ export function jdwpListeningPort(line: string): number | null {
}
/**
* Module directory (relative to the project root, `""` for the root) of a JVM
* source file: the part before `src/{main,test}/…`, else the working directory
* of the closest Maven / Gradle configuration.
* Module directory (relative to the project root, `""` for the root; absolute for a
* module outside it) of a JVM source file. Like IDEA's `ProjectFileIndex.getModuleForFile`:
* the innermost Maven / Gradle module folder holding the file, as the build model knows
* them (`modules`). Before a model knows a deeper one: the part before
* `src/{main,test}/…`, else the working directory of the closest Maven / Gradle configuration.
*/
export function jvmModuleDir(relPath: string, configs: readonly RunConfig[] = []): string {
export function jvmModuleDir(
relPath: string,
configs: readonly RunConfig[] = [],
modules: Iterable<string> = useBuildPresentation.getState().modules.keys(),
): string {
const owner = [...modules].filter((d) => d && relPath.startsWith(d + "/")).sort((a, b) => b.length - a.length)[0];
if (owner) return owner;
const m = /^(?:(.*)\/)?src\/(?:main|test|[\w-]+)\/(?:java|kotlin|scala|groovy)\//.exec(relPath);
if (m) return m[1] ?? "";
const cwds = configs
+10 -2
Переглянути файл
@@ -80,7 +80,14 @@ export interface LanguagesValid {
jdk: string;
jdtlsJdk: string;
jdtlsVmArgs: string;
gradle: { jvm: string; distribution: "wrapper" | "local"; home: string; userHome: string };
gradle: {
jvm: string;
distribution: "wrapper" | "local";
home: string;
userHome: string;
/** IDEA's "Build and run using": Gradle (`JavaExec`) or the IDE (the language server's classpath). */
runUsing: "gradle" | "ide";
};
maven: { home: string; userSettings: string; localRepository: string; unlinkedProjects: string[] };
};
/** Preview font size in px; `null` follows the editor font size. */
@@ -124,7 +131,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = {
jdk: "",
jdtlsJdk: "",
jdtlsVmArgs: "",
gradle: { jvm: "", distribution: "wrapper", home: "", userHome: "" },
gradle: { jvm: "", distribution: "wrapper", home: "", userHome: "", runUsing: "gradle" },
maven: { home: "", userSettings: "", localRepository: "", unlinkedProjects: [] },
},
markdown: { previewFontSize: null },
@@ -250,6 +257,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
distribution: oneOf(gradle.distribution, ["wrapper", "local"] as const, d.java.gradle.distribution),
home: text(gradle.home, d.java.gradle.home),
userHome: text(gradle.userHome, d.java.gradle.userHome),
runUsing: oneOf(gradle.runUsing, ["gradle", "ide"] as const, d.java.gradle.runUsing),
},
maven: {
home: text(maven.home, d.java.maven.home),
+3 -1
Переглянути файл
@@ -5703,5 +5703,7 @@
"Usage limits": "用量限额",
"The account's 5-hour and weekly limits, when the agent reports them (Claude Code)": "账户的 5 小时和每周限额,在智能体报告时显示(Claude Code)",
"Session cost": "会话花费",
"What the session has cost so far, as the agent reports it (API pricing, also shown on subscriptions)": "智能体报告的本会话目前花费(按 API 价格计算,订阅账户也会显示)"
"What the session has cost so far, as the agent reports it (API pricing, also shown on subscriptions)": "智能体报告的本会话目前花费(按 API 价格计算,订阅账户也会显示)",
"Build and run using": "构建和运行使用",
"Gradle runs a main class as a JavaExec task on its source set's runtime classpath (Gradle's resource processing included). OpenIDEA launches it on the Java language server's classpath.": "Gradle 以 JavaExec 任务在源集的运行时类路径上运行主类(包括 Gradle 的资源处理)。OpenIDEA 在 Java 语言服务器的类路径上启动它。"
}