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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-03 22:02:40 +08:00
1 джерело 9dbae1d189
коміт ae406ca2fb
11 files changed
+476 -3

No files matched your search

+92
Переглянути файл
@@ -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();
});
+24
Переглянути файл
@@ -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);