дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(plugins): report Problems to plugins in remote projects (#2223)
Цей коміт міститься в:
1 джерело
fd8b4ff14c
коміт
3dac5b8bdc
8 files changed
+199
-7
No files matched your search
@@ -1,6 +1,11 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Diagnostic, FileProblems } from "@/bindings";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
projectChanged: () => {},
|
||||
remoteChanged: () => {},
|
||||
report: vi.fn(),
|
||||
}));
|
||||
const reports = vi.hoisted(() => [] as [boolean, FileProblems[]][]);
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/platform/ipc")>()),
|
||||
@@ -8,12 +13,29 @@ vi.mock("@/platform/ipc", async (importOriginal) => ({
|
||||
commands: {
|
||||
mcpReportProblems: async (reset: boolean, files: FileProblems[]) => {
|
||||
reports.push([reset, files]);
|
||||
await state.report();
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/platform/project", () => ({
|
||||
useProject: { getState: () => ({ project: { root: "/p" } }), subscribe: () => () => {} },
|
||||
useProject: {
|
||||
getState: () => ({ project: { root: "/p" } }),
|
||||
subscribe: (fn: (s: unknown, prev: unknown) => void) => {
|
||||
state.projectChanged = () => fn({ project: { root: "/p" } }, { project: null });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/platform/remote", () => ({
|
||||
isRemoteWindow: () => true,
|
||||
useRemote: {
|
||||
subscribe: (fn: (s: unknown, prev: unknown) => void) => {
|
||||
state.remoteChanged = () => fn({ session: {} }, { session: {} });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { applyDiagnosticsEvent, clearProblems } = await import("./problemsStore");
|
||||
@@ -35,6 +57,7 @@ describe("reportProblems", () => {
|
||||
vi.useFakeTimers();
|
||||
clearProblems();
|
||||
reports.length = 0;
|
||||
state.report.mockReset();
|
||||
applyDiagnosticsEvent("/p/a.rs", "ra", [d(1), d(2)]);
|
||||
stop = reportProblems();
|
||||
});
|
||||
@@ -43,6 +66,35 @@ describe("reportProblems", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it.each(["projectChanged", "remoteChanged"] as const)("resends the full snapshot after %s", async (change) => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
state[change]();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(summary()).toEqual([
|
||||
[true, [["/p/a.rs", 2]]],
|
||||
[true, [["/p/a.rs", 2]]],
|
||||
]);
|
||||
});
|
||||
|
||||
it("retries a failed delta with the full snapshot", async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
state.report.mockRejectedValueOnce(new Error("disconnected"));
|
||||
applyDiagnosticsEvent("/p/b.rs", "ra", [d(0)]);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(summary()).toEqual([
|
||||
[true, [["/p/a.rs", 2]]],
|
||||
[false, [["/p/b.rs", 1]]],
|
||||
[
|
||||
true,
|
||||
[
|
||||
["/p/a.rs", 2],
|
||||
["/p/b.rs", 1],
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends everything first, then only the files that changed once diagnostics settle", async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
applyDiagnosticsEvent("/p/b.rs", "ra", [d(0)]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Reports changed files from the Problems view to the built-in MCP host. */
|
||||
import { commands, call, type FileProblems } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { isRemoteWindow } from "@/platform/remote";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
import { useProblems, type ProblemsMap } from "./problemsStore";
|
||||
|
||||
const REPORT_MS = 1000;
|
||||
@@ -13,7 +13,7 @@ export function reportProblems(): () => void {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const flush = () => {
|
||||
timer = null;
|
||||
if (isRemoteWindow() || !useProject.getState().project) return;
|
||||
if (!useProject.getState().project) return;
|
||||
const map = useProblems.getState().map;
|
||||
const files: FileProblems[] = [];
|
||||
for (const [path, sources] of map)
|
||||
@@ -42,8 +42,16 @@ export function reportProblems(): () => void {
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
const offRemote = useRemote.subscribe((s, prev) => {
|
||||
if (s.session !== prev.session) {
|
||||
reset = true;
|
||||
reported = new Map();
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
disposed = true;
|
||||
offRemote();
|
||||
offProject();
|
||||
off();
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { activeEditor, onEditorEvent, openEditorPaths, useEditorRegistry } from "@/editor/api";
|
||||
import { commands, call, onIdeEvent, ideEventsReady } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { isRemoteWindow } from "@/platform/remote";
|
||||
import { isRemoteWindow, useRemote } from "@/platform/remote";
|
||||
|
||||
import { registerOverlay } from "@/workbench/contributions";
|
||||
import { McpConfirmHost } from "./ConfirmDialog";
|
||||
@@ -85,6 +85,13 @@ export function activate(): () => void {
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
const offRemote = useRemote.subscribe((s, prev) => {
|
||||
if (s.session !== prev.session) {
|
||||
lastPaths = undefined;
|
||||
lastEditor = undefined;
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
window.addEventListener("focus", schedule);
|
||||
window.addEventListener("blur", schedule);
|
||||
return () => {
|
||||
@@ -96,6 +103,7 @@ export function activate(): () => void {
|
||||
window.removeEventListener("beforeunload", unload);
|
||||
offRegistry();
|
||||
offEvents();
|
||||
offRemote();
|
||||
offProject();
|
||||
window.removeEventListener("focus", schedule);
|
||||
window.removeEventListener("blur", schedule);
|
||||
|
||||
@@ -7,6 +7,7 @@ const state = vi.hoisted(() => ({
|
||||
registryChanged: () => {},
|
||||
editorChanged: () => {},
|
||||
projectChanged: () => {},
|
||||
remoteChanged: () => {},
|
||||
reportOpen: vi.fn(async () => undefined),
|
||||
reportFocused: vi.fn(async () => undefined),
|
||||
}));
|
||||
@@ -44,7 +45,15 @@ vi.mock("@/platform/project", () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/platform/remote", () => ({ isRemoteWindow: () => state.remote }));
|
||||
vi.mock("@/platform/remote", () => ({
|
||||
isRemoteWindow: () => state.remote,
|
||||
useRemote: {
|
||||
subscribe: (fn: (s: unknown, prev: unknown) => void) => {
|
||||
state.remoteChanged = () => fn({ session: {} }, { session: {} });
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/platform/ipc", () => ({
|
||||
call: (promise: Promise<unknown>) => promise,
|
||||
onIdeEvent: () => () => {},
|
||||
@@ -106,6 +115,14 @@ describe("MCP editor reports", () => {
|
||||
expect(state.reportOpen).toHaveBeenCalledTimes(2);
|
||||
expect(state.reportFocused).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("resends unchanged editors after reconnecting at the same root", async () => {
|
||||
state.remote = true;
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
state.remoteChanged();
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
expect(state.reportOpen).toHaveBeenCalledTimes(2);
|
||||
expect(state.reportFocused).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it("forwards remote editor reports and stops after disposal", async () => {
|
||||
state.remote = true;
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
|
||||
@@ -56,6 +56,33 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("remote client settings", () => {
|
||||
it("routes window reports through the commands advertised by the remote host", async () => {
|
||||
enableRemote({
|
||||
target: { kind: "ssh", config: "test" },
|
||||
root: "/remote",
|
||||
host: "test",
|
||||
os: "linux",
|
||||
commands: ["mcp_set_open_editors", "mcp_set_focused_editor", "mcp_report_problems"],
|
||||
});
|
||||
invoke.mockResolvedValue("null");
|
||||
await call(commands.mcpSetOpenEditors(["/remote/a.txt"]));
|
||||
await call(commands.mcpSetFocusedEditor(null, true));
|
||||
const files = [{ path: "/remote/a.txt", items: [] }];
|
||||
await call(commands.mcpReportProblems(true, files));
|
||||
await call(commands.mcpReportProblems(false, files));
|
||||
const reports = invoke.mock.calls.map(([cmd, args]) => [
|
||||
cmd,
|
||||
(args as { cmd: string }).cmd,
|
||||
JSON.parse((args as { args: string }).args),
|
||||
]);
|
||||
expect(reports).toEqual([
|
||||
["remote_invoke", "mcp_set_open_editors", { paths: ["/remote/a.txt"] }],
|
||||
["remote_invoke", "mcp_set_focused_editor", { editor: null, focused: true }],
|
||||
["remote_invoke", "mcp_report_problems", { reset: true, files }],
|
||||
["remote_invoke", "mcp_report_problems", { reset: false, files }],
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ gradle: { dir: "app", projects: [] }, expected: [{ dir: "app", projects: [] }] },
|
||||
{
|
||||
|
||||
Reference in new issue
Заблокувати користувача