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();