From cde0dbd1841fa66281808738f624173a416a796b Mon Sep 17 00:00:00 2001 From: badcuban Date: Mon, 25 May 2026 02:26:54 -0400 Subject: [PATCH] fix: include directory junctions in project browser --- .../workspace/Layers/WorkspaceEntries.test.ts | 21 ++++++ .../src/workspace/Layers/WorkspaceEntries.ts | 69 +++++++++++++++---- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts b/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts index 84ea5c51937..0f48d457ded 100644 --- a/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts +++ b/apps/server/src/workspace/Layers/WorkspaceEntries.test.ts @@ -360,6 +360,27 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => { }), ); + it.effect("includes directory symlinks when browsing", () => + Effect.gen(function* () { + const workspaceEntries = yield* WorkspaceEntries; + const path = yield* Path.Path; + const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-symlink-" }); + const target = path.join(cwd, "target"); + const link = path.join(cwd, "linked"); + yield* writeTextFile(cwd, "target/index.ts", "export {};\n"); + yield* Effect.promise(() => + fsPromises.symlink(target, link, process.platform === "win32" ? "junction" : "dir"), + ); + + const result = yield* workspaceEntries.browse({ + partialPath: appendSeparator(cwd), + }); + + expect(result.entries).toContainEqual({ name: "linked", fullPath: link }); + expect(result.entries).toContainEqual({ name: "target", fullPath: target }); + }), + ); + it.effect("supports relative paths when cwd is provided", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries; diff --git a/apps/server/src/workspace/Layers/WorkspaceEntries.ts b/apps/server/src/workspace/Layers/WorkspaceEntries.ts index 0c0ab638207..95e09b8d188 100644 --- a/apps/server/src/workspace/Layers/WorkspaceEntries.ts +++ b/apps/server/src/workspace/Layers/WorkspaceEntries.ts @@ -11,7 +11,11 @@ import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; -import { type FilesystemBrowseInput, type ProjectEntry } from "@t3tools/contracts"; +import { + type FilesystemBrowseEntry, + type FilesystemBrowseInput, + type ProjectEntry, +} from "@t3tools/contracts"; import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; import { insertRankedSearchResult, @@ -44,6 +48,18 @@ const IGNORED_DIRECTORY_NAMES = new Set([ "out", ".cache", ]); +const WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES = new Set([ + "Application Data", + "Cookies", + "Local Settings", + "My Documents", + "NetHood", + "PrintHood", + "Recent", + "SendTo", + "Start Menu", + "Templates", +]); interface WorkspaceIndex { scannedAt: number; @@ -62,6 +78,25 @@ function toPosixPath(input: string): string { return input.replaceAll("\\", "/"); } +async function isDirectoryEntry(dirent: Dirent, fullPath: string): Promise { + if (dirent.isDirectory()) { + return true; + } + if (!dirent.isSymbolicLink()) { + return false; + } + if (process.platform === "win32" && WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES.has(dirent.name)) { + return false; + } + + try { + const stat = await fsPromises.stat(fullPath); + return stat.isDirectory(); + } catch { + return false; + } +} + function expandHomePath(input: string, path: Path.Path): string { if (input === "~") { return OS.homedir(); @@ -456,20 +491,30 @@ export const makeWorkspaceEntries = Effect.gen(function* () { const showHidden = endsWithSeparator || prefix.startsWith("."); const lowerPrefix = prefix.toLowerCase(); + const directoryEntries = yield* Effect.forEach( + dirents, + (dirent) => + Effect.promise(async (): Promise => { + const fullPath = path.join(parentPath, dirent.name); + if ( + !dirent.name.toLowerCase().startsWith(lowerPrefix) || + (!showHidden && dirent.name.startsWith(".")) || + !(await isDirectoryEntry(dirent, fullPath)) + ) { + return null; + } + return { + name: dirent.name, + fullPath, + }; + }), + { concurrency: 16 }, + ); return { parentPath, - entries: dirents - .filter( - (dirent) => - dirent.isDirectory() && - dirent.name.toLowerCase().startsWith(lowerPrefix) && - (showHidden || !dirent.name.startsWith(".")), - ) - .map((dirent) => ({ - name: dirent.name, - fullPath: path.join(parentPath, dirent.name), - })) + entries: directoryEntries + .filter((entry): entry is FilesystemBrowseEntry => entry !== null) .toSorted((left, right) => left.name.localeCompare(right.name)), }; },