fix(run): drop a moved file's temporary context configuration (#2037)

A single file's context configuration keeps the path it was produced
for, so moving main.cxx out of src/ left 'context:file:src/main.cxx'
selected while the gutter produced a second configuration for the new
path — the run command still compiled the old file.

fsChanged now drops temporary context configurations (context:file: and
the compiled context:build: partner) whose file was removed, the way
IDEA deletes the context configuration with the file. Saved and
detected configurations stay.

Fixes l0ng-ai/openidea#2035
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-08 02:42:42 +08:00
1 джерело 27e73ea2cb
коміт dfcdd8d764
3 files changed
+59 -1

No files matched your search

+3
Переглянути файл
@@ -71,6 +71,7 @@ import {
resetRuns,
runningSessions,
saveUserConfigs,
dropStaleContextConfigs,
selectConfig,
selectedConfig,
startRun,
@@ -654,6 +655,8 @@ export function activate(): () => void {
disposers.push(
onIdeEvent("fsChanged", (e) => {
const paths = [...e.created, ...e.modified, ...e.removed];
// A file that moved or was deleted leaves its context configuration behind.
dropStaleContextConfigs(e.removed);
if (paths.some((p) => /(?:^|\/)go\.(?:mod|work)$/.test(p))) invalidateGoModules();
if (paths.some((p) => p.endsWith("/__init__.py"))) invalidatePythonPackages();
if (paths.some((p) => CONFIG_FILES.test(p))) scheduleReload();
+30 -1
Переглянути файл
@@ -19,7 +19,15 @@ import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { EditConfigsOverlay } from "./EditConfigsDialog";
import { ConfigChooserOverlay } from "./ConfigChooser";
import { adoptTemporary, canDeleteConfig, deleteConfig, loadConfigs, resetRuns, useRun } from "./store";
import {
adoptTemporary,
canDeleteConfig,
deleteConfig,
dropStaleContextConfigs,
loadConfigs,
resetRuns,
useRun,
} from "./store";
import { registerBuiltinTypes } from "./types";
const config = (kind = "npm"): RunConfig => ({
@@ -112,6 +120,27 @@ it("temporary deletion does not persist suppression", async () => {
expect(useRun.getState().configs).toEqual([]);
});
it("a moved file drops its stale temporary context configuration", async () => {
const old: RunConfig = { ...config("cc"), id: "context:file:src/main.cxx", name: "main.cxx", detected: false };
adoptTemporary(old);
expect(useRun.getState().configs.map((c) => c.id)).toContain("context:file:src/main.cxx");
dropStaleContextConfigs(["/test-removal/src/main.cxx"]);
expect(useRun.getState().configs.map((c) => c.id)).not.toContain("context:file:src/main.cxx");
expect(useRun.getState().temporary).toEqual([]);
expect(ipc.runSaveConfigs).not.toHaveBeenCalled();
});
it("keeps other temporary and non-temporary configurations on removal", async () => {
const moved: RunConfig = { ...config("cc"), id: "context:file:src/main.cxx", name: "main.cxx", detected: false };
const other: RunConfig = { ...config("go"), id: "context:file:app/main.go", name: "main.go", detected: false };
adoptTemporary(moved);
adoptTemporary(other);
dropStaleContextConfigs(["/test-removal/src/main.cxx"]);
expect(useRun.getState().temporary).toEqual([other.id]);
// The saved/detected npm config and an unrelated file stay.
expect(useRun.getState().configs.map((c) => c.id)).toEqual(expect.arrayContaining(["npm:app", other.id]));
});
it("chooser popup Delete removes the selected detection", async () => {
useRun.setState({ chooserOpen: true, editorOpen: false });
render(<ConfigChooserOverlay />);
+26
Переглянути файл
@@ -411,6 +411,32 @@ export function adoptTemporary(config: RunConfig): RunConfig {
return config;
}
/**
* A file removed (or moved away) takes the temporary context configurations its
* path produced (`context:file:` and the compiled `context:build:` partner), like
* IDEA deleting the context configuration with the file. Saved and detected
* configurations the user made stay.
*/
export function dropStaleContextConfigs(removed: readonly string[]) {
const { configs, temporary, selectedId } = useRun.getState();
const root = useProject.getState().project?.root;
if (!root || !removed.length || !temporary.length) return;
// Temporary ids carry the project-relative path (the full path outside the root).
const gone = new Set(removed.map((p) => (p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p)));
const stale = (id: string) => {
const m = /^context:(?:file|build):(.+?)(?:#.+)?$/.exec(id);
return !!m && gone.has(m[1]!);
};
const dropped = temporary.filter(stale);
if (!dropped.length) return;
useRun.setState({
configs: configs.filter((c) => !dropped.includes(c.id)),
temporary: temporary.filter((id) => !stale(id)),
});
persistTemporary();
if (selectedId && dropped.includes(selectedId)) selectConfig(null);
}
/** "Save Configuration": a temporary configuration becomes a user configuration. */
export async function saveTemporary(id: string): Promise<void> {
const { configs, temporary } = useRun.getState();