Skip to content

Commit 8faa7ab

Browse files
badcubanjuliusmarminge
authored andcommitted
fix: include directory junctions in project browser
1 parent 60b546c commit 8faa7ab

2 files changed

Lines changed: 78 additions & 14 deletions

File tree

‎apps/server/src/workspace/WorkspaceEntries.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,28 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => {
334334
}),
335335
);
336336

337+
it.effect("includes directory symlinks when browsing", () =>
338+
Effect.gen(function* () {
339+
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
340+
const path = yield* Path.Path;
341+
const platform = yield* HostProcessPlatform;
342+
const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-symlink-" });
343+
const target = path.join(cwd, "target");
344+
const link = path.join(cwd, "linked");
345+
yield* writeTextFile(cwd, "target/index.ts", "export {};\n");
346+
yield* Effect.promise(() =>
347+
fsPromises.symlink(target, link, platform === "win32" ? "junction" : "dir"),
348+
);
349+
350+
const result = yield* workspaceEntries.browse({
351+
partialPath: yield* appendSeparator(cwd),
352+
});
353+
354+
expect(result.entries).toContainEqual({ name: "linked", fullPath: link });
355+
expect(result.entries).toContainEqual({ name: "target", fullPath: target });
356+
}),
357+
);
358+
337359
it.effect("supports relative paths when cwd is provided", () =>
338360
Effect.gen(function* () {
339361
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;

‎apps/server/src/workspace/WorkspaceEntries.ts‎

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @effect-diagnostics nodeBuiltinImport:off
2+
import type { Dirent } from "node:fs";
23
import * as NodeFSP from "node:fs/promises";
34
import * as NodeOS from "node:os";
45

@@ -60,6 +61,41 @@ export class WorkspaceEntries extends Context.Service<
6061
}
6162
>()("t3/workspace/WorkspaceEntries") {}
6263

64+
const WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES = new Set([
65+
"Application Data",
66+
"Cookies",
67+
"Local Settings",
68+
"My Documents",
69+
"NetHood",
70+
"PrintHood",
71+
"Recent",
72+
"SendTo",
73+
"Start Menu",
74+
"Templates",
75+
]);
76+
77+
async function isDirectoryEntry(
78+
dirent: Dirent,
79+
fullPath: string,
80+
platform: NodeJS.Platform,
81+
): Promise<boolean> {
82+
if (dirent.isDirectory()) {
83+
return true;
84+
}
85+
if (!dirent.isSymbolicLink()) {
86+
return false;
87+
}
88+
if (platform === "win32" && WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES.has(dirent.name)) {
89+
return false;
90+
}
91+
92+
try {
93+
return (await NodeFSP.stat(fullPath)).isDirectory();
94+
} catch {
95+
return false;
96+
}
97+
}
98+
6399
function expandHomePath(input: string, path: Path.Path): string {
64100
if (input === "~") {
65101
return NodeOS.homedir();
@@ -103,6 +139,7 @@ const resolveBrowseTarget = (
103139

104140
const make = Effect.gen(function* () {
105141
const path = yield* Path.Path;
142+
const platform = yield* HostProcessPlatform;
106143
const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
107144
const workspaceSearchIndexes = yield* WorkspaceSearchIndex.WorkspaceSearchIndexMap;
108145

@@ -177,23 +214,28 @@ const make = Effect.gen(function* () {
177214

178215
const showHidden = endsWithSeparator || prefix.startsWith(".");
179216
const lowerPrefix = prefix.toLowerCase();
180-
const entries: Array<{ readonly name: string; readonly fullPath: string }> = [];
181-
for (const dirent of dirents) {
182-
if (
183-
dirent.isDirectory() &&
184-
dirent.name.toLowerCase().startsWith(lowerPrefix) &&
185-
(showHidden || !dirent.name.startsWith("."))
186-
) {
187-
entries.push({
188-
name: dirent.name,
189-
fullPath: path.join(parentPath, dirent.name),
190-
});
191-
}
192-
}
217+
const entries = yield* Effect.forEach(
218+
dirents,
219+
(dirent) =>
220+
Effect.promise(async () => {
221+
const fullPath = path.join(parentPath, dirent.name);
222+
if (
223+
!dirent.name.toLowerCase().startsWith(lowerPrefix) ||
224+
(!showHidden && dirent.name.startsWith(".")) ||
225+
!(await isDirectoryEntry(dirent, fullPath, platform))
226+
) {
227+
return null;
228+
}
229+
return { name: dirent.name, fullPath };
230+
}),
231+
{ concurrency: 16 },
232+
);
193233

194234
return {
195235
parentPath,
196-
entries: entries.toSorted((left, right) => left.name.localeCompare(right.name)),
236+
entries: entries
237+
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
238+
.toSorted((left, right) => left.name.localeCompare(right.name)),
197239
};
198240
},
199241
);

0 commit comments

Comments
 (0)