дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(gradle): link and unlink independent Gradle builds (#1413)
Opening a folder of independent Gradle builds selected only the first build. Link Gradle Projects… (pick a build's settings or build file) and Unlink on a build's root let an ordinary project window manage several independent builds without changing their scripts. The build model holds a list of Gradle builds; each keeps its own wrapper, working directory, environment, task routing and sync cache, keyed by the owning build directory. The links persist in the project's Gradle settings (linkedProjects); unlinking leaves files and saved run configurations alone. Existing includeBuild support is unchanged, and an explicitly linked build takes precedence over a composite that also includes it. Workspace windows keep Link/Unlink disabled. Co-authored-by: gudcks0305 <[email protected]>
Цей коміт міститься в:
1 джерело
682fee633d
коміт
0dfdbbc323
43 files changed
+1846
-254
No files matched your search
@@ -0,0 +1,120 @@
|
||||
/** Linked Gradle builds use isolated native stubs; no real builds or files are touched. */
|
||||
import type { GradleModel, RunConfig } from "../src/platform/ipc";
|
||||
import { expect, ipcCalls, stubDesktop, test } from "./fixtures";
|
||||
|
||||
const build = (dir: string): GradleModel => ({
|
||||
dir,
|
||||
command: `/dev/acme-api/${dir}/gradlew`,
|
||||
env: { JAVA_HOME: `/${dir}/jdk` },
|
||||
javaVersion: 21,
|
||||
syncedAt: 1,
|
||||
stale: false,
|
||||
syncError: null,
|
||||
projects: [
|
||||
{ path: ":", name: dir, dir, buildFile: `${dir}/build.gradle`, tasks: [] },
|
||||
{
|
||||
path: ":app",
|
||||
name: "app",
|
||||
dir: `${dir}/app`,
|
||||
buildFile: `${dir}/app/build.gradle`,
|
||||
tasks: [{ name: "assemble", group: "build", description: "Assembles this project" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
test("links independent Gradle roots, runs identical task paths in their own builds and unlinks without deletion", async ({
|
||||
workbench: page,
|
||||
}, info) => {
|
||||
await stubDesktop(page);
|
||||
await page.evaluate(
|
||||
async ({ first, second }) => {
|
||||
let linked = [first];
|
||||
let runId = 0;
|
||||
const model = () => ({ maven: null, gradle: linked });
|
||||
const h = window.__ipc.handlers;
|
||||
h.build_model = model;
|
||||
h.build_sync = model;
|
||||
h.run_configs = () => [];
|
||||
h.run_apply_settings = (args) => args.configs;
|
||||
h.gradle_link_projects = () => {
|
||||
if (!linked.some((g) => g.dir === second.dir)) linked = [...linked, second];
|
||||
return model();
|
||||
};
|
||||
h.gradle_unlink_projects = (args) => {
|
||||
linked = linked.filter((g) => !(args.paths as string[]).includes(g.dir));
|
||||
return model();
|
||||
};
|
||||
h["plugin:dialog|open"] = () => ["/dev/acme-api/second/settings.gradle"];
|
||||
h.run_start = (args) => {
|
||||
const config = args.config as RunConfig;
|
||||
const id = ++runId;
|
||||
setTimeout(() => window.__ipc.emit({ type: "runExited", run: id, code: 0 }), 50);
|
||||
return { id, configId: config.id, name: config.name, pid: 4242 + id, running: true, pty: false };
|
||||
};
|
||||
const storeModule = "/src/features/build-tools/store.ts";
|
||||
const { loadModel } = await import(/* @vite-ignore */ storeModule);
|
||||
await loadModel();
|
||||
const contributionsModule = "/src/workbench/contributions.ts";
|
||||
const { toolWindowService } = await import(/* @vite-ignore */ contributionsModule);
|
||||
toolWindowService().show("gradle", true);
|
||||
},
|
||||
{ first: build("first"), second: build("second") },
|
||||
);
|
||||
|
||||
const tree = page.getByRole("tree", { name: "Gradle projects" });
|
||||
await expect(tree.getByRole("treeitem", { name: "first", exact: true })).toBeVisible();
|
||||
await expect(tree.getByRole("treeitem", { name: "second", exact: true })).toHaveCount(0);
|
||||
await page.screenshot({ path: info.outputPath("gradle-one-root.png"), animations: "disabled" });
|
||||
|
||||
await page.getByRole("button", { name: "Link Gradle Projects", exact: true }).click();
|
||||
await expect
|
||||
.poll(() => ipcCalls(page, "gradle_link_projects"))
|
||||
.toEqual([{ paths: ["/dev/acme-api/second/settings.gradle"] }]);
|
||||
await expect(tree.getByRole("treeitem", { name: "second", exact: true })).toBeVisible();
|
||||
// Reloading the model reads the persisted linkage again.
|
||||
await page.evaluate(async () => {
|
||||
const storeModule = "/src/features/build-tools/store.ts";
|
||||
const { loadModel } = await import(/* @vite-ignore */ storeModule);
|
||||
await loadModel();
|
||||
const actionModule = "/src/platform/actions.ts";
|
||||
const { executeAction } = await import(/* @vite-ignore */ actionModule);
|
||||
await executeAction("gradle.expandAll");
|
||||
await executeAction("gradle.toggleOffline");
|
||||
});
|
||||
const tasks = tree.getByRole("treeitem", { name: /^assemble\b/ });
|
||||
await expect(tasks).toHaveCount(2);
|
||||
await page.screenshot({ path: info.outputPath("gradle-two-roots.png"), animations: "disabled" });
|
||||
|
||||
await tasks.nth(0).dblclick();
|
||||
await expect.poll(async () => (await ipcCalls(page, "run_start")).length).toBe(1);
|
||||
await tasks.nth(1).dblclick();
|
||||
await expect.poll(async () => (await ipcCalls(page, "run_start")).length).toBe(2);
|
||||
const runs = (await ipcCalls(page, "run_start")).map((args) => args.config as RunConfig);
|
||||
expect(runs[0]!.id).not.toBe(runs[1]!.id);
|
||||
for (const [index, dir] of ["first", "second"].entries()) {
|
||||
expect(runs[index]).toMatchObject({
|
||||
command: `/dev/acme-api/${dir}/gradlew`,
|
||||
cwd: dir,
|
||||
args: [":app:assemble", "--offline"],
|
||||
env: { JAVA_HOME: `/${dir}/jdk` },
|
||||
});
|
||||
}
|
||||
|
||||
for (const dir of ["second", "first"]) {
|
||||
await tree.getByRole("treeitem", { name: dir, exact: true }).click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Unlink Gradle Project", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Unlink Gradle project?" });
|
||||
await expect(dialog).toContainText("Files will remain");
|
||||
await dialog.getByRole("button", { name: "Unlink", exact: true }).click();
|
||||
await expect(tree.getByRole("treeitem", { name: dir, exact: true })).toHaveCount(0);
|
||||
}
|
||||
await expect
|
||||
.poll(() => ipcCalls(page, "gradle_unlink_projects"))
|
||||
.toEqual([{ paths: ["second"] }, { paths: ["first"] }]);
|
||||
await expect(page.getByText("No Gradle projects", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Link Gradle Projects", exact: true })).toBeVisible();
|
||||
expect(
|
||||
await page.evaluate(() => window.__ipc.calls.filter((c) => /^fs_(?:delete|remove|trash)/.test(c.cmd))),
|
||||
).toEqual([]);
|
||||
await page.screenshot({ path: info.outputPath("gradle-unlinked.png"), animations: "disabled" });
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача