Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { TreeBookkeeping } from "../../utils/treeBookkeeping";
import { purgeDirectory, rekeyDirectory } from "../../utils/treeBookkeeping";
import {
asDirectoryHandle,
lookupDirectory,
resolveDeleteTreePath,
stripTrailingSlash,
toAbs,
Expand Down Expand Up @@ -484,8 +485,9 @@ export function useFilesTabBridge({
if (!loadedDirsRef.current.has(newDir)) {
unloadedDirCandidatesRef.current.add(newDir);
}
const handle = asDirectoryHandle(model.getItem(newKey));
if (handle?.isExpanded()) void fetchDir(newDir);
if (lookupDirectory(model, newKey)?.isExpanded()) {
void fetchDir(newDir);
}
}
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
asDirectoryHandle,
basename,
lookupDirectory,
parentRel,
resolveDeleteTreePath,
stripTrailingSlash,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test";
import { resolveDeleteTreePath } from "./treePath";
import { FileTree } from "@pierre/trees";
import { lookupDirectory, resolveDeleteTreePath } from "./treePath";

describe("resolveDeleteTreePath", () => {
it("infers a tracked directory when watcher metadata is absent", () => {
Expand Down Expand Up @@ -33,3 +34,32 @@ describe("resolveDeleteTreePath", () => {
});
});
});

describe("lookupDirectory", () => {
// The watcher stats a path to decide `isDirectory` and reports false when
// the stat loses a race with the rename that produced the event, so a
// directory can enter the tree as a file. Every later lookup beneath it
// then walks into a node that has no child index.
const treeHoldingBuildAsAFile = () => {
const model = new FileTree({ paths: ["src/out/keep.ts"] });
model.add("build");
return model;
};

it("yields no handle for a path the model cannot resolve", () => {
const model = treeHoldingBuildAsAFile();
expect(() => model.getItem("build/out/")).toThrow();
expect(lookupDirectory(model, "build/out/")).toBeNull();
});

it("still reports an expanded directory the model does hold", () => {
const model = treeHoldingBuildAsAFile();
lookupDirectory(model, "src/out/")?.expand();
expect(lookupDirectory(model, "src/out/")?.isExpanded()).toBe(true);
});

it("yields no handle for a file", () => {
const model = treeHoldingBuildAsAFile();
expect(lookupDirectory(model, "src/out/keep.ts")).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
FileTree,
FileTreeDirectoryHandle,
FileTreeItemHandle,
} from "@pierre/trees";
Expand Down Expand Up @@ -62,3 +63,25 @@ export function asDirectoryHandle(
): FileTreeDirectoryHandle | null {
return handle?.isDirectory() ? (handle as FileTreeDirectoryHandle) : null;
}

/**
* The directory handle for `treePath`, or null when Pierre can't produce one.
*
* `getItem` is typed to return null for a path Pierre doesn't hold, but it
* throws instead when the lookup walks through a node with no child index —
* a path whose ancestor Pierre holds as a file. That happens when the watcher
* loses a stat race and reports a directory with `isDirectory: false`, so the
* fs-event handler adds it as a file and every later lookup beneath it throws.
* Callers use the handle only to ask whether a directory is already expanded,
* and not being able to ask is the same answer as "not expanded".
*/
export function lookupDirectory(
model: Pick<FileTree, "getItem">,
treePath: string,
): FileTreeDirectoryHandle | null {
try {
return asDirectoryHandle(model.getItem(treePath));
} catch {
return null;
}
}
Loading