From 28d0790c26b394f9dada9128f93ed1ec21c8eeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 25 Apr 2026 18:35:14 +0200 Subject: [PATCH] fix(app): normalize watcher paths --- packages/app/src/context/file/watcher.test.ts | 55 +++++++++++++++++++ packages/app/src/context/file/watcher.ts | 20 ++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/file/watcher.test.ts b/packages/app/src/context/file/watcher.test.ts index 9536b52536b6..4a2698d1a496 100644 --- a/packages/app/src/context/file/watcher.test.ts +++ b/packages/app/src/context/file/watcher.test.ts @@ -58,6 +58,61 @@ describe("file watcher invalidation", () => { expect(loads).toEqual(["src/open.ts"]) }) + test("normalizes watcher paths before matching open files", () => { + const loads: string[] = [] + + invalidateFromWatcher( + { + type: "file.watcher.updated", + properties: { + file: "src\\open.ts", + event: "change", + }, + }, + { + normalize: (input) => input, + hasFile: () => false, + isOpen: (path) => path === "src/open.ts", + loadFile: (path) => loads.push(path), + node: () => ({ + path: "src/open.ts", + type: "file", + name: "open.ts", + absolute: "/repo/src/open.ts", + ignored: false, + }), + isDirLoaded: () => false, + refreshDir: () => {}, + }, + ) + + expect(loads).toEqual(["src/open.ts"]) + }) + + test("refreshes nearest loaded ancestor on add", () => { + const refresh: string[] = [] + + invalidateFromWatcher( + { + type: "file.watcher.updated", + properties: { + file: "src/nested/deep/new.ts", + event: "add", + }, + }, + { + normalize: (input) => input, + hasFile: () => false, + loadFile: () => {}, + node: () => undefined, + isDirLoaded: (path) => path === "src", + refreshDir: (path) => refresh.push(path), + }, + ) + + expect(refresh).toEqual(["src"]) + }) + test("refreshes only changed loaded directory nodes", () => { const refresh: string[] = [] diff --git a/packages/app/src/context/file/watcher.ts b/packages/app/src/context/file/watcher.ts index fbf71992791a..cff481f5cf3b 100644 --- a/packages/app/src/context/file/watcher.ts +++ b/packages/app/src/context/file/watcher.ts @@ -15,6 +15,20 @@ type WatcherOps = { refreshDir: (path: string) => void } +function toWatcherKey(path: string) { + return path.replace(/\\/g, "/") +} + +function nearestLoadedParent(path: string, ops: WatcherOps) { + const parts = path.split("/").slice(0, -1) + while (true) { + const dir = parts.join("/") + if (ops.isDirLoaded(dir)) return dir + if (parts.length === 0) return + parts.pop() + } +} + export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { if (event.type !== "file.watcher.updated") return const props = @@ -24,7 +38,7 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { if (!rawPath) return if (!kind) return - const path = ops.normalize(rawPath) + const path = toWatcherKey(ops.normalize(rawPath)) if (!path) return if (path.startsWith(".git/")) return @@ -46,8 +60,8 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { } if (kind !== "add" && kind !== "unlink") return - const parent = path.split("/").slice(0, -1).join("/") - if (!ops.isDirLoaded(parent)) return + const parent = nearestLoadedParent(path, ops) + if (parent === undefined) return ops.refreshDir(parent) }