From dfcdd8d764e7011ab7655cf0a9ad0ac24b30f1c8 Mon Sep 17 00:00:00 2001 From: arookieofc Date: Sun, 8 Nov 2026 02:42:42 +0800 Subject: [PATCH] fix(run): drop a moved file's temporary context configuration (#2037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/features/run/index.ts | 3 +++ src/features/run/removal.test.tsx | 31 ++++++++++++++++++++++++++++++- src/features/run/store.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/features/run/index.ts b/src/features/run/index.ts index 4f8670ecf..bca9c6987 100644 --- a/src/features/run/index.ts +++ b/src/features/run/index.ts @@ -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(); diff --git a/src/features/run/removal.test.tsx b/src/features/run/removal.test.tsx index 29733e03a..567ae03ff 100644 --- a/src/features/run/removal.test.tsx +++ b/src/features/run/removal.test.tsx @@ -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(); diff --git a/src/features/run/store.ts b/src/features/run/store.ts index 3e3b25314..eab344fff 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -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 { const { configs, temporary } = useRun.getState();