дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(java): resolve Maven projects with the profiles chosen in the Maven tool window (#1044)
The profiles checked in the Maven tool window only reached `mvn` (sync and goal runs) and the Kotlin bridge. jdtls imported every module without them, so a dependency whose version, or module, comes from a profile stayed unresolved and its classes were reported as missing, while Build Project ran without `-P` as well. - A Maven sync records the profiles it ran with. jdtls gets a copy of the user settings.xml with them in `<activeProfiles>` (m2e has no other input for profiles), sent as a setting so a running server re-imports instead of restarting; the file name follows its content so m2e reloads it. - Toggling a profile reloads the Maven projects (once for several toggles in a row, again after a sync that started with the previous choice). - Build Project passes the synced profiles with `-P`. - When jdtls finishes an import or project update it does not re-check open files, so their errors stayed until the next edit; open Java files are now re-checked on its ProjectStatus report. This also covers a POM edit or a dependency added to ~/.m2. Fixes #993
Цей коміт міститься в:
1 джерело
9dbae1d189
коміт
ae406ca2fb
11 files changed
+476
-3
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import type { BuildModel, MavenModel, ProjectInfo } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { loadModel, PROFILE_RELOAD_DELAY_MS, resetBuild, setProfile, 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 = {
|
||||
syncedAt: 1,
|
||||
stale: false,
|
||||
syncError: null,
|
||||
profiles: [
|
||||
{ id: "native", activeByDefault: false },
|
||||
{ id: "local", activeByDefault: false },
|
||||
{ id: "dev", activeByDefault: true },
|
||||
],
|
||||
projects: [],
|
||||
} as unknown as MavenModel;
|
||||
const model: BuildModel = { maven, gradle: null };
|
||||
|
||||
/** The `-P` entries each sync was started with. */
|
||||
const syncedProfiles = () => mocks.sync.mock.calls.map((c) => c[3] as string[]);
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
resetBuild();
|
||||
useBuild.setState({ syncing: {} });
|
||||
useProject.setState({ project: { root: "/p", trusted: true } as ProjectInfo });
|
||||
mocks.model.mockResolvedValue(model);
|
||||
await loadModel();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reloads Maven once with the new profiles after a few toggles", async () => {
|
||||
let finish = () => {};
|
||||
mocks.sync.mockImplementation(() => new Promise((resolve) => (finish = () => resolve(model))));
|
||||
setProfile("native", true);
|
||||
setProfile("local", true);
|
||||
setProfile("dev", false);
|
||||
expect(mocks.sync).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
expect(syncedProfiles()).toEqual([["native", "local", "!dev"]]);
|
||||
finish();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(mocks.sync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reloads again after a sync that started with the previous profiles", async () => {
|
||||
const finishes: (() => void)[] = [];
|
||||
mocks.sync.mockImplementation(() => new Promise((resolve) => finishes.push(() => resolve(model))));
|
||||
setProfile("native", true);
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
setProfile("local", true);
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
expect(syncedProfiles()).toEqual([["native"]]);
|
||||
finishes[0]!();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(syncedProfiles()).toEqual([["native"], ["native", "local"]]);
|
||||
});
|
||||
|
||||
it("does not reload a project closed before the delay ran out", async () => {
|
||||
setProfile("native", true);
|
||||
resetBuild();
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
expect(mocks.sync).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -114,8 +114,19 @@ export const toggleSkipTests = () =>
|
||||
export const toggleGradleOffline = () =>
|
||||
updateSettings((s) => ({ ...s, gradle: { ...s.gradle, offline: !s.gradle.offline } }));
|
||||
|
||||
/** Delay before reloading Maven after a profile toggle, so a few toggles in a row reload once. */
|
||||
export const PROFILE_RELOAD_DELAY_MS = 800;
|
||||
let profileReload: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
export function setProfile(id: string, checked: boolean) {
|
||||
updateSettings((s) => ({ ...s, maven: { ...s.maven, profiles: { ...s.maven.profiles, [id]: checked } } }));
|
||||
// The Java language server and Build Project resolve the project with the profiles of the
|
||||
// last sync, so a new choice reloads it (IDEA re-imports on a profile change too).
|
||||
clearTimeout(profileReload);
|
||||
profileReload = setTimeout(() => {
|
||||
profileReload = undefined;
|
||||
requestSync("maven");
|
||||
}, PROFILE_RELOAD_DELAY_MS);
|
||||
}
|
||||
|
||||
export function readHistory(tool: BuildTool): string[] {
|
||||
@@ -269,6 +280,15 @@ export function initialExpanded(tool: BuildTool, model: BuildModel): Set<string>
|
||||
|
||||
const TITLES: Record<BuildTool, string> = { maven: "Syncing Maven projects", gradle: "Syncing Gradle projects" };
|
||||
|
||||
/** Tools to sync again once their running sync ends: its options changed meanwhile. */
|
||||
const queuedSyncs = new Set<BuildTool>();
|
||||
|
||||
/** Syncs `tool` now, or right after the sync in progress (which used the previous options). */
|
||||
export function requestSync(tool: BuildTool) {
|
||||
if (useBuild.getState().syncing[tool]) queuedSyncs.add(tool);
|
||||
else void syncTool(tool);
|
||||
}
|
||||
|
||||
/** Reloads all projects of `tool` from the build tool itself (effective POM, dependency tree, task list). */
|
||||
export async function syncTool(tool: BuildTool): Promise<void> {
|
||||
const state = useBuild.getState();
|
||||
@@ -317,6 +337,7 @@ export async function syncTool(tool: BuildTool): Promise<void> {
|
||||
} finally {
|
||||
if (useBuild.getState().syncing[tool]?.request === request) setSync(undefined);
|
||||
upsertTask({ id: taskId, title: TITLES[tool], message: null, fraction: 1, done: true });
|
||||
if (queuedSyncs.delete(tool) && gen === generation) void syncTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +351,9 @@ export function cancelSync(tool: BuildTool) {
|
||||
|
||||
export function resetBuild() {
|
||||
generation++;
|
||||
clearTimeout(profileReload);
|
||||
profileReload = undefined;
|
||||
queuedSyncs.clear();
|
||||
useAnalyzer.setState({ pom: null });
|
||||
for (const tool of ["maven", "gradle"] as const) {
|
||||
cancelSync(tool);
|
||||
|
||||
Reference in new issue
Заблокувати користувача