mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(maven): reload a stale Maven build after external changes to its files (#780)
When a pom.xml changes outside the IDE (a VCS update, another tool) and the model is stale afterwards, the Maven build now reloads on its own; edits saved from the editor still only show "Load Maven Changes". A change counts as external unless the IDE itself saved that file moments before, so a build file open in a tab that a tool rewrites also reloads. Co-authored-by: ntbang0901 <121003585+ntbang0901@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
import type { BuildModel, MavenModel, ProjectInfo } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { changedOutsideIde, loadModel, noteExternalBuildChange, noteIdeSave, useBuild } from "./store";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ model: vi.fn(), sync: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
isTauri: () => true,
|
||||
call: (value: unknown) => value,
|
||||
commands: {
|
||||
...real.commands,
|
||||
buildModel: mocks.model,
|
||||
runConfigs: () => Promise.resolve([]),
|
||||
buildSync: mocks.sync,
|
||||
cancelRequest: () => Promise.resolve(),
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@tauri-apps/api/core", async (original) => ({
|
||||
...(await original<typeof import("@tauri-apps/api/core")>()),
|
||||
Channel: class {
|
||||
onmessage: unknown = null;
|
||||
},
|
||||
}));
|
||||
|
||||
const maven = (stale: boolean) =>
|
||||
({ syncedAt: 1, stale, syncError: null, profiles: [], projects: [] }) as unknown as MavenModel;
|
||||
const model = (stale: boolean): BuildModel => ({ maven: maven(stale), gradle: null });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.sync.mockImplementation(() => new Promise(() => {}));
|
||||
useBuild.setState({ syncing: {} });
|
||||
useProject.setState({ project: { root: "/p", trusted: true } as ProjectInfo });
|
||||
});
|
||||
|
||||
it("reloads a build made stale by a change outside the editor, not by an edit in it", async () => {
|
||||
mocks.model.mockResolvedValue(model(true));
|
||||
await loadModel();
|
||||
expect(mocks.sync).not.toHaveBeenCalled();
|
||||
|
||||
noteExternalBuildChange();
|
||||
await loadModel();
|
||||
expect(mocks.sync).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.sync.mock.calls[0]![0]).toBe("maven");
|
||||
expect(useBuild.getState().model?.maven?.stale).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves an up-to-date build alone after an external change", async () => {
|
||||
mocks.model.mockResolvedValue(model(false));
|
||||
noteExternalBuildChange();
|
||||
await loadModel();
|
||||
expect(mocks.sync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tells the IDE's own saves of a build file apart from changes made outside it", () => {
|
||||
noteIdeSave("/p/pom.xml", 1000);
|
||||
expect(changedOutsideIde(["/p/pom.xml"], 2000)).toBe(false);
|
||||
// A tool rewrites the same file later, while it is still open in a tab.
|
||||
expect(changedOutsideIde(["/p/pom.xml"], 10_000)).toBe(true);
|
||||
// Never saved by the IDE: a VCS update.
|
||||
expect(changedOutsideIde(["/p/core/pom.xml"], 2000)).toBe(true);
|
||||
noteIdeSave("/p/pom.xml", 20_000);
|
||||
expect(changedOutsideIde(["/p/pom.xml", "/p/core/pom.xml"], 20_500)).toBe(true);
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
* project has that build system, like IDEA. The model is re-read when build
|
||||
* files change; running goals / tasks goes through the run feature's actions.
|
||||
*/
|
||||
import { onEditorEvent } from "@/editor/api";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { onIdeEvent, type BuildModel, type BuildTool } from "@/platform/ipc";
|
||||
@@ -25,6 +26,9 @@ import {
|
||||
unlinkProjects,
|
||||
cancelSync,
|
||||
loadModel,
|
||||
changedOutsideIde,
|
||||
noteExternalBuildChange,
|
||||
noteIdeSave,
|
||||
openAnalyzer,
|
||||
resetBuild,
|
||||
setExpanded,
|
||||
@@ -158,8 +162,16 @@ export function activate(): () => void {
|
||||
resetBuild();
|
||||
if (s.project) void loadModel();
|
||||
}),
|
||||
onEditorEvent((e) => {
|
||||
if (e.type === "docSaved" && BUILD_FILE.test(e.path)) noteIdeSave(e.path);
|
||||
}),
|
||||
onIdeEvent("fsChanged", (e) => {
|
||||
if ([...e.created, ...e.modified, ...e.removed].some((p) => BUILD_FILE.test(p))) scheduleLoad();
|
||||
const changed = [...e.created, ...e.modified, ...e.removed].filter((p) => BUILD_FILE.test(p));
|
||||
if (!changed.length) return;
|
||||
// Changes the IDE's own saves didn't make came from outside (a VCS update, another tool),
|
||||
// also for a build file open in a tab.
|
||||
if (changedOutsideIde(changed)) noteExternalBuildChange();
|
||||
scheduleLoad();
|
||||
}),
|
||||
onIdeEvent("projectRootsChanged", scheduleLoad),
|
||||
// Project JDK / Maven home and options are applied to the model's launchers.
|
||||
|
||||
@@ -142,6 +142,41 @@ export function pushHistory(tool: BuildTool, line: string) {
|
||||
// Model
|
||||
|
||||
let generation = 0;
|
||||
let externalChange = false;
|
||||
|
||||
/**
|
||||
* A build file changed outside the editor (VCS update, another tool): the next model load
|
||||
* reloads a build it finds stale, IDEA's default "Reload after external changes". Edits in
|
||||
* the editor only show "Load … Changes".
|
||||
*/
|
||||
export function noteExternalBuildChange() {
|
||||
externalChange = true;
|
||||
}
|
||||
|
||||
/** How long after the IDE saves a build file its file-system events still count as that save. */
|
||||
const IDE_SAVE_WINDOW_MS = 5000;
|
||||
/** Build files the IDE saved, with when: their file-system events are edits made in the IDE. */
|
||||
const savedByIde = new Map<string, number>();
|
||||
|
||||
/** The IDE saved `path` (an editor save, not a tool rewriting it). */
|
||||
export function noteIdeSave(path: string, now = Date.now()) {
|
||||
savedByIde.set(path, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any of the changed build files was written by something other than the IDE's own
|
||||
* save: a VCS update or a tool, also when the file is open in an editor tab.
|
||||
*/
|
||||
export function changedOutsideIde(paths: readonly string[], now = Date.now()): boolean {
|
||||
let external = false;
|
||||
for (const path of paths) {
|
||||
const at = savedByIde.get(path);
|
||||
if (at !== undefined && now - at <= IDE_SAVE_WINDOW_MS) continue;
|
||||
savedByIde.delete(path);
|
||||
external = true;
|
||||
}
|
||||
return external;
|
||||
}
|
||||
|
||||
/** Re-reads the build model (and the run configurations) of the open project. */
|
||||
export async function loadModel(): Promise<void> {
|
||||
@@ -167,6 +202,11 @@ export async function loadModel(): Promise<void> {
|
||||
]);
|
||||
if (gen !== generation) return;
|
||||
useBuild.setState({ model, configs, loading: false, error: null });
|
||||
if (externalChange) {
|
||||
externalChange = false;
|
||||
const maven = model.maven;
|
||||
if (maven?.stale && maven.syncedAt !== null && !useBuild.getState().syncing.maven) void syncTool("maven");
|
||||
}
|
||||
} catch (e) {
|
||||
if (gen === generation) useBuild.setState({ loading: false, error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user