fix: sync inspection results with profiles and restore CSS validation

Цей коміт міститься в:
l0ng-ai committed 2026-10-25 23:41:15 +08:00
1 джерело d3feb056aa
коміт 6b9a2ec67e
9 files changed
+387 -12

No files matched your search

+8 -2
Переглянути файл
@@ -5,15 +5,21 @@
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { registerOverlay, registerToolWindow } from "@/workbench/contributions";
import { showCodeCleanupDialog, showDuplicatesDialog, showInspectCodeDialog, showStructuralDialog } from "./AnalysisDialogs";
import {
showCodeCleanupDialog,
showDuplicatesDialog,
showInspectCodeDialog,
showStructuralDialog,
} from "./AnalysisDialogs";
import { AnalysisDialogHost } from "./dialogs";
import { resetDuplicates } from "./duplicates";
import { DuplicatesHeaderActions, DuplicatesView } from "./DuplicatesView";
import { resetInspection } from "./inspect";
import { installInspectionProfileSync, resetInspection } from "./inspect";
import { InspectionHeaderActions, InspectionView, useInspectionHeaderMenu } from "./InspectionView";
export function activate(): () => void {
const disposers = [
installInspectionProfileSync(),
useProject.subscribe((s, prev) => {
if (s.project?.root === prev.project?.root) return;
resetInspection();
+99
Переглянути файл
@@ -0,0 +1,99 @@
import { afterEach, expect, it, vi } from "vitest";
import type { Diagnostic, InspectEvent } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { installInspectionProfileSync, resetInspection, runInspection, useInspection } from "./inspect";
const mock = vi.hoisted(() => ({ channel: null as null | { onmessage: (event: InspectEvent) => void } }));
vi.mock("@tauri-apps/api/core", () => ({
Channel: class {
onmessage = () => {};
},
}));
vi.mock("@/platform/ipc", async (original) => ({
...(await original<object>()),
isTauri: () => true,
call: (value: unknown) => value,
commands: {
inspectCode: (_id: unknown, _scope: unknown, channel: typeof mock.channel) => {
mock.channel = channel;
return Promise.resolve();
},
cancelRequest: () => Promise.resolve(),
},
}));
vi.mock("@/workbench/contributions", () => ({
toolWindowService: () => ({ show: () => {} }),
notifications: () => ({ notify: () => {} }),
}));
const diagnostic: Diagnostic = {
range: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } },
severity: "error",
source: "ts",
server: "typescript-language-server",
code: "2322",
message: "Wrong type",
unnecessary: false,
deprecated: false,
};
function profile(override: object) {
useProject.setState({ settings: { inspections: { default: { "lsp:typescript-language-server": override } } } });
}
function file(path = "/p/a.ts") {
mock.channel!.onmessage({ type: "file", done: 1, file: { path, relPath: path.slice(3), diagnostics: [diagnostic] } });
}
function done() {
mock.channel!.onmessage({ type: "done", summary: { problems: 1 } } as InspectEvent);
}
afterEach(() => {
resetInspection();
vi.useRealTimers();
});
it("reprojects completed results, counts and original severities without rerunning", async () => {
const dispose = installInspectionProfileSync();
try {
profile({});
await runInspection({ kind: "project", directory: "" }, "default");
file();
done();
const excluded = new Set(["excluded"]);
useInspection.setState({ excluded });
profile({ severity: "warning" });
expect(useInspection.getState().files.get("/p/a.ts")!.diagnostics[0]!.severity).toBe("warning");
profile({ enabled: false });
expect(useInspection.getState().files.size).toBe(0);
expect(useInspection.getState().summary!.problems).toBe(0);
profile({});
expect(useInspection.getState().files.get("/p/a.ts")!.diagnostics[0]!.severity).toBe("error");
expect(useInspection.getState().summary!.problems).toBe(1);
expect(useInspection.getState().excluded).toBe(excluded);
resetInspection();
expect(useInspection.getState().rawFiles.size).toBe(0);
} finally {
dispose();
}
});
it("retains initially disabled diagnostics and uses edits made during streaming", async () => {
vi.useFakeTimers();
const dispose = installInspectionProfileSync();
try {
profile({ enabled: false });
await runInspection({ kind: "project", directory: "" }, "default");
file();
vi.advanceTimersByTime(150);
expect(useInspection.getState().files.size).toBe(0);
profile({ severity: "warning" });
expect(useInspection.getState().files.size).toBe(1);
file("/p/b.ts");
done();
expect([...useInspection.getState().files.values()].map((f) => f.diagnostics[0]!.severity)).toEqual([
"warning",
"warning",
]);
expect(useInspection.getState().summary!.problems).toBe(2);
} finally {
dispose();
}
});
+36 -7
Переглянути файл
@@ -22,6 +22,7 @@ import {
profileOverrides,
type ProfileId,
} from "@/platform/inspections";
import { useProject } from "@/platform/project";
import { notifications, toolWindowService } from "@/workbench/contributions";
import { describeScope, resolveScope, type ScopeChoice } from "./scope";
@@ -100,6 +101,8 @@ interface InspectState {
running: boolean;
total: number;
done: number;
/** Unfiltered server results, retained so disabled inspections can be restored. */
rawFiles: Map<string, InspectedFileResult>;
/** Files with problems, by path. */
files: Map<string, InspectedFileResult>;
summary: InspectSummary | null;
@@ -117,6 +120,7 @@ const initialInspection = (): InspectState => ({
running: false,
total: 0,
done: 0,
rawFiles: new Map(),
files: new Map(),
summary: null,
scopeLabel: null,
@@ -141,6 +145,28 @@ export function toggleExcludedProblems(problems: readonly InspectionProblem[]) {
export const useInspection = create<InspectState>(initialInspection);
/** Rebuild from server diagnostics rather than previously overridden severities. */
function projectResults(rawFiles: Map<string, InspectedFileResult>, profileId: ProfileId) {
const profile = profileOverrides(profileId);
const files = new Map<string, InspectedFileResult>();
for (const [path, file] of rawFiles) {
const diagnostics = applyProfileMixed(profile, file.diagnostics);
if (diagnostics.length) files.set(path, { ...file, diagnostics });
}
return { files, problems: [...files.values()].reduce((n, f) => n + f.diagnostics.length, 0) };
}
/** Keep both completed and streaming results in sync with edits to their chosen profile. */
export function installInspectionProfileSync() {
return useProject.subscribe((state, prev) => {
if (state.settings?.inspections === prev.settings?.inspections) return;
const { rawFiles, profile, summary } = useInspection.getState();
if (profile === null) return;
const { files, problems } = projectResults(rawFiles, profile);
useInspection.setState({ files, summary: summary ? { ...summary, problems } : null });
});
}
let inflight: RequestId | null = null;
export function cancelInspection() {
@@ -169,6 +195,7 @@ export async function runInspection(choice: ScopeChoice, profileId: ProfileId =
running: true,
total: 0,
done: 0,
rawFiles: new Map(),
files: new Map(),
summary: null,
error: null,
@@ -177,7 +204,6 @@ export async function runInspection(choice: ScopeChoice, profileId: ProfileId =
profile: profileId,
excluded: new Set(),
});
const profile = profileOverrides(profileId);
toolWindowService().show("inspection");
if (!isTauri()) {
useInspection.setState({ running: false, error: "Inspect Code needs the desktop app" });
@@ -187,7 +213,8 @@ export async function runInspection(choice: ScopeChoice, profileId: ProfileId =
let flush: ReturnType<typeof setTimeout> | null = null;
const publish = () => {
flush = null;
if (inflight === id) useInspection.setState({ files: new Map(found) });
if (inflight === id)
useInspection.setState({ rawFiles: new Map(found), files: projectResults(found, profileId).files });
};
const channel = new Channel<InspectEvent>();
channel.onmessage = (ev) => {
@@ -197,23 +224,25 @@ export async function runInspection(choice: ScopeChoice, profileId: ProfileId =
useInspection.setState({ total: ev.total });
break;
case "file": {
const diagnostics = applyProfileMixed(profile, ev.file.diagnostics);
for (const d of ev.file.diagnostics) if (d.code) noteDiagnosticCodes(producerOf(d), [d]);
if (diagnostics.length) found.set(ev.file.path, { ...ev.file, diagnostics });
found.set(ev.file.path, ev.file);
useInspection.setState({ done: ev.done });
flush ??= setTimeout(publish, 150);
break;
}
case "done":
case "done": {
const { files, problems } = projectResults(found, profileId);
if (flush) clearTimeout(flush);
inflight = null;
useInspection.setState({
running: false,
files: new Map(found),
rawFiles: new Map(found),
files,
// Problems the profile kept, not all the servers reported.
summary: { ...ev.summary, problems: [...found.values()].reduce((n, f) => n + f.diagnostics.length, 0) },
summary: { ...ev.summary, problems },
});
break;
}
}
};
try {