diff --git a/packages/server/src/server/file-observer/internal/paths.ts b/packages/server/src/server/file-observer/internal/paths.ts index 8d75156ca9..fd943e0a72 100644 --- a/packages/server/src/server/file-observer/internal/paths.ts +++ b/packages/server/src/server/file-observer/internal/paths.ts @@ -53,7 +53,7 @@ export function createObserverPaths(platform: NodeJS.Platform): ObserverPaths { isInside, collapse, createIgnoreMatcher(roots) { - return createPathContainmentMatcher(roots, platform === "win32"); + return createPathContainmentMatcher({ roots, compareAsWindows: platform === "win32" }); }, normalizeIgnoredRoots(root, paths) { const inside = paths diff --git a/packages/server/src/server/git-observation-paths.ts b/packages/server/src/server/git-observation-paths.ts index e37f0539f9..9516402469 100644 --- a/packages/server/src/server/git-observation-paths.ts +++ b/packages/server/src/server/git-observation-paths.ts @@ -81,7 +81,10 @@ export class GitObservationPaths { excluded.push(this.windows ? subtree.toLowerCase() : subtree); } } - const isExcluded = createPathContainmentMatcher(excluded, this.windows); + const isExcluded = createPathContainmentMatcher({ + roots: excluded, + compareAsWindows: this.windows, + }); return (candidate) => { const normalized = this.normalizeEventPath(candidate); const comparable = this.windows ? normalized.toLowerCase() : normalized; diff --git a/packages/server/src/server/workspace-git-service.ts b/packages/server/src/server/workspace-git-service.ts index 40cc1b5c15..d822447af4 100644 --- a/packages/server/src/server/workspace-git-service.ts +++ b/packages/server/src/server/workspace-git-service.ts @@ -2045,10 +2045,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { } private pruneKnownDirectories(target: WorkingTreeWatchTarget): void { - const isIgnored = createPathContainmentMatcher( - target.ignoredDirectories, - looksLikeDefiniteWindowsPath(target.watchPath), - ); + const isIgnored = createPathContainmentMatcher({ + roots: target.ignoredDirectories, + compareAsWindows: looksLikeDefiniteWindowsPath(target.watchPath), + }); for (const directory of target.knownDirectories) { if (isIgnored(directory)) { target.knownDirectories.delete(directory); diff --git a/packages/server/src/utils/path.test.ts b/packages/server/src/utils/path.test.ts index 34399473c1..318f33e410 100644 --- a/packages/server/src/utils/path.test.ts +++ b/packages/server/src/utils/path.test.ts @@ -101,7 +101,10 @@ describe("path equivalence", () => { describe("indexed path containment", () => { test("preserves POSIX case and component boundaries", () => { - const matches = createPathContainmentMatcher(["/repo/Build/", "/repo/cache"], false); + const matches = createPathContainmentMatcher({ + roots: ["/repo/Build/", "/repo/cache"], + compareAsWindows: false, + }); expect(matches("/repo/Build/nested/file")).toBe(true); expect(matches("/repo/cache")).toBe(true); expect(matches("/repo/Build-sibling/file")).toBe(false); @@ -110,10 +113,10 @@ describe("indexed path containment", () => { }); test("normalizes Windows case, separators and extended namespaces", () => { - const matches = createPathContainmentMatcher( - ["C:/Repo/Build", String.raw`\\server\share\cache`], - true, - ); + const matches = createPathContainmentMatcher({ + roots: ["C:/Repo/Build", String.raw`\\server\share\cache`], + compareAsWindows: true, + }); expect(matches(String.raw`\\?\C:\REPO\build\file`)).toBe(true); expect(matches(String.raw`\\?\UNC\SERVER\SHARE\Cache\file`)).toBe(true); expect(matches("C:/Repo/Build-Sibling/file")).toBe(false); @@ -121,8 +124,14 @@ describe("indexed path containment", () => { }); test("handles filesystem roots and empty sets", () => { - expect(createPathContainmentMatcher(["/"], false)("/repo/file")).toBe(true); - expect(createPathContainmentMatcher(["C:/"], true)("c:/repo/file")).toBe(true); - expect(createPathContainmentMatcher([], false)("/repo/file")).toBe(false); + expect( + createPathContainmentMatcher({ roots: ["/"], compareAsWindows: false })("/repo/file"), + ).toBe(true); + expect( + createPathContainmentMatcher({ roots: ["C:/"], compareAsWindows: true })("c:/repo/file"), + ).toBe(true); + expect(createPathContainmentMatcher({ roots: [], compareAsWindows: false })("/repo/file")).toBe( + false, + ); }); }); diff --git a/packages/server/src/utils/path.ts b/packages/server/src/utils/path.ts index d21a6a16ee..b693956047 100644 --- a/packages/server/src/utils/path.ts +++ b/packages/server/src/utils/path.ts @@ -72,11 +72,16 @@ export function stopPathContainmentMetrics(): number { return checks; } +interface PathContainmentOptions { + roots: Iterable; + compareAsWindows: boolean; +} + /** Compile lexical subtree membership without scanning unrelated roots per candidate. */ -export function createPathContainmentMatcher( - roots: Iterable, - compareAsWindows: boolean, -): (candidate: string) => boolean { +export function createPathContainmentMatcher({ + roots, + compareAsWindows, +}: PathContainmentOptions): (candidate: string) => boolean { const pathApi = compareAsWindows ? nodePath.win32 : nodePath.posix; const indexed = new Set( Array.from(roots, (root) => normalizePathForComparison(root, compareAsWindows)),