Skip to content
Draft
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
51 changes: 37 additions & 14 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "@t3tools/client-runtime/state/filesystem";
import {
appendBrowsePathSegment,
getBrowseDirectoryPath,
inferProjectTitleFromPath,
isWindowsPlatform,
} from "@t3tools/client-runtime/state/projects";
Expand Down Expand Up @@ -304,7 +305,7 @@ function useBrowsePathInput(environment: EnvironmentOption | null, pinnedDirecto
if (environment && canPreloadBrowsePath(environmentRuntime?.connectionState)) {
await loadBrowsePath({
environmentId: environment.environmentId,
input: { partialPath: selectedDirectoryPath },
input: { partialPath: selectedDirectoryPath, fuzzy: true },
});
}
},
Expand Down Expand Up @@ -766,12 +767,32 @@ function FolderBrowser(props: {
readonly pinnedDirectoryName?: string;
}) {
const browsePath = useMemo(
() => getFilesystemBrowsePath(props.pathInput, props.environment.platform),
[props.environment.platform, props.pathInput],
() =>
getFilesystemBrowsePath(
props.pathInput,
props.environment.platform,
true,
props.pinnedDirectoryName ? "" : getAddProjectInitialQuery(props.environment.baseDirectory),
),
[
props.environment.platform,
props.environment.baseDirectory,
props.pathInput,
props.pinnedDirectoryName,
],
);
// A pinned repository folder does not exist yet; search the selected parent.
const pinnedDirectoryName = props.pinnedDirectoryName ?? "";
const pinnedDirectoryMatches = isWindowsPlatform(props.environment.platform)
? browsePath.filterQuery.toLowerCase() === pinnedDirectoryName.toLowerCase()
: browsePath.filterQuery === pinnedDirectoryName;
const browseFilterQuery = pinnedDirectoryMatches ? "" : browsePath.filterQuery;
const browseInput = useMemo(
() => (browsePath.directoryPath.length > 0 ? { partialPath: browsePath.directoryPath } : null),
[browsePath.directoryPath],
() =>
browsePath.directoryPath.length > 0
? { partialPath: `${browsePath.directoryPath}${browseFilterQuery}`, fuzzy: true }
: null,
[browsePath.directoryPath, browseFilterQuery],
);
const browseState = useEnvironmentQuery(
browseInput === null
Expand All @@ -781,13 +802,6 @@ function FolderBrowser(props: {
input: browseInput,
}),
);
// A pinned repository folder does not exist yet, so filtering the listing by
// it would empty the folder picker. Anything the user typed still filters.
const pinnedDirectoryName = props.pinnedDirectoryName ?? "";
const pinnedDirectoryMatches = isWindowsPlatform(props.environment.platform)
? browsePath.filterQuery.toLowerCase() === pinnedDirectoryName.toLowerCase()
: browsePath.filterQuery === pinnedDirectoryName;
const browseFilterQuery = pinnedDirectoryMatches ? "" : browsePath.filterQuery;
const { visibleEntries: visibleBrowseEntries } = useMemo(
() => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browseFilterQuery),
[browseFilterQuery, browseState.data?.entries],
Expand Down Expand Up @@ -829,6 +843,7 @@ function FolderBrowser(props: {
<ListRow
key={entry.fullPath}
title={entry.name}
subtitle={entry.fullPath}
icon={
<SymbolView
name="folder"
Expand All @@ -841,7 +856,7 @@ function FolderBrowser(props: {
right={null}
onPress={() => {
void props.navigateToBrowsePath({
browseDirectoryPath: browsePath.directoryPath,
browseDirectoryPath: getBrowseDirectoryPath(entry.fullPath),
selectedDirectoryName: entry.name,
});
}}
Expand All @@ -863,8 +878,16 @@ export function AddProjectLocalFolderScreen(props: { readonly environmentId?: st
const submitPath = useCallback(async () => {
if (!environment || isBrowseNavigating || isSubmitting) return;
setError(null);
const browsePath = getFilesystemBrowsePath(
pathInput,
environment.platform,
true,
getAddProjectInitialQuery(environment.baseDirectory),
);
const resolved = resolveAddProjectPath({
rawPath: pathInput,
rawPath: browsePath.isBrowsing
? `${browsePath.directoryPath}${browsePath.filterQuery}`
: pathInput,
currentProjectCwd: null,
platform: environment.platform,
});
Expand Down
263 changes: 263 additions & 0 deletions apps/server/src/workspace/WorkspaceEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,269 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => {
});

describe("browse", () => {
it.effect("bounds directory reads for ambiguous compact queries", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const cwd = yield* makeTempDir();
for (let index = 0; index < 140; index += 1) {
yield* writeTextFile(cwd, `workspace-${index}/makespace/index.ts`);
}
vi.mocked(NodeFSP.readdir).mockClear();
const result = yield* workspaceEntries.browse({
cwd,
partialPath: "./wormak",
fuzzy: true,
});
expect(result.entries.length).toBeGreaterThan(0);
expect(vi.mocked(NodeFSP.readdir).mock.calls.length).toBeLessThanOrEqual(128);
}),
);

it.effect("matches compact queries across folder names and returns each path once", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "wor/unrelated/index.ts");
yield* writeTextFile(cwd, "Workspace/makespace/index.ts");
const result = yield* workspaceEntries.browse({
cwd,
partialPath: "./wormak",
fuzzy: true,
});
expect(result.entries).toEqual([
{
name: "makespace",
fullPath: path.join(cwd, "Workspace/makespace"),
searchMatch: { query: "wormak", score: expect.any(Number) },
},
]);
}),
);

it.effect("matches compact queries over three levels and tolerates fragment typos", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "Workspace/projects/makespace/index.ts");
for (const query of ["worprjmak", "worprjmkaes"]) {
const result = yield* workspaceEntries.browse({
cwd,
partialPath: `./${query}`,
fuzzy: true,
});
expect(result.entries).toEqual([
{
name: "makespace",
fullPath: path.join(cwd, "Workspace/projects/makespace"),
searchMatch: { query, score: expect.any(Number) },
},
]);
}
}),
);

it.effect("retains literal folder names without searching their descendants", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "wormak/index.ts");
yield* writeTextFile(cwd, "Workspace/makespace/index.ts");
vi.mocked(NodeFSP.readdir).mockClear();
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./wormak", fuzzy: true })).entries,
).toEqual([{ name: "wormak", fullPath: path.join(cwd, "wormak") }]);
expect(NodeFSP.readdir).toHaveBeenCalledTimes(1);
}),
);

it.effect("keeps compact searches out of hidden and nonmatching subtrees", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, ".Workspace/makespace/index.ts");
yield* writeTextFile(cwd, "Archive/Workspace/makespace/index.ts");
vi.mocked(NodeFSP.readdir).mockClear();
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./wormak", fuzzy: true })).entries,
).toEqual([]);
expect(vi.mocked(NodeFSP.readdir).mock.calls.map(([directory]) => directory)).toEqual([
cwd,
]);
}),
);

it.effect("does not widen a relative search beyond its requested root", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "wor/unrelated/index.ts");
vi.mocked(NodeFSP.readdir).mockClear();
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./wor/zzzzz", fuzzy: true }))
.entries,
).toEqual([]);
expect(
vi
.mocked(NodeFSP.readdir)
.mock.calls.some(([directory]) => directory === path.dirname(cwd)),
).toBe(false);
expect(NodeFSP.readdir).toHaveBeenCalledTimes(2);
}),
);

it.effect("continues through fuzzy parents when an exact abbreviation is a dead end", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "wor/unrelated/index.ts");
yield* writeTextFile(cwd, "Workspace/makespace/index.ts");
const result = yield* workspaceEntries.browse({
cwd,
partialPath: "./wor/mak",
fuzzy: true,
});
expect(result.entries).toEqual([
{ name: "makespace", fullPath: path.join(cwd, "Workspace/makespace") },
]);
}),
);

it.effect("backs up past an existing parent when a later abbreviated level has no match", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "wor/projects/unrelated/index.ts");
yield* writeTextFile(cwd, "Workspace/projects/makespace/index.ts");
const result = yield* workspaceEntries.browse({
cwd,
partialPath: "./wor/prj/mak",
fuzzy: true,
});
expect(result.entries).toEqual([
{ name: "makespace", fullPath: path.join(cwd, "Workspace/projects/makespace") },
]);
}),
);

it.effect("keeps empty exact directories and exact paths with matching children", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const fileSystem = yield* FileSystem.FileSystem;
const cwd = yield* makeTempDir();
yield* fileSystem.makeDirectory(path.join(cwd, "wor"));
yield* writeTextFile(cwd, "Workspace/makespace/index.ts");
expect(yield* workspaceEntries.browse({ cwd, partialPath: "./wor/", fuzzy: true })).toEqual(
{ parentPath: path.join(cwd, "wor"), entries: [] },
);
yield* writeTextFile(cwd, "wor/makers/index.ts");
vi.mocked(NodeFSP.readdir).mockClear();
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./wor/mak", fuzzy: true })).entries,
).toEqual([{ name: "makers", fullPath: path.join(cwd, "wor/makers") }]);
expect(NodeFSP.readdir).toHaveBeenCalledTimes(1);
}),
);

it.effect("resolves abbreviations and typos across multiple parent directories", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "Workspace/projects/t3code/index.ts");
yield* writeTextFile(cwd, "Downloads/unrelated/index.ts");

for (const partialPath of ["./wrk/prj/t3cd", "./workspcae/projcts/t3cdoe"]) {
const result = yield* workspaceEntries.browse({ cwd, partialPath, fuzzy: true });
expect(result).toEqual({
parentPath: path.join(cwd, "Workspace/projects"),
entries: [{ name: "t3code", fullPath: path.join(cwd, "Workspace/projects/t3code") }],
});
}
expect(
vi
.mocked(NodeFSP.readdir)
.mock.calls.some(([directory]) => directory === path.join(cwd, "Downloads")),
).toBe(false);
}),
);

it.effect("preserves equally named results from ambiguous parent directories", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "work/t3code/index.ts");
yield* writeTextFile(cwd, "Workspace/t3code/index.ts");
const result = yield* workspaceEntries.browse({
cwd,
partialPath: "./wrk/t3",
fuzzy: true,
});
expect(result.entries).toEqual([
{ name: "t3code", fullPath: path.join(cwd, "work/t3code") },
{ name: "t3code", fullPath: path.join(cwd, "Workspace/t3code") },
]);
}),
);

it.effect("keeps exact directories on the single-listing path", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, "work/t3code/index.ts");
yield* writeTextFile(cwd, "Workspace/another/index.ts");
vi.mocked(NodeFSP.readdir).mockClear();
const result = yield* workspaceEntries.browse({ cwd, partialPath: "./work/", fuzzy: true });
expect(result.entries).toEqual([
{ name: "t3code", fullPath: path.join(cwd, "work/t3code") },
]);
expect(NodeFSP.readdir).toHaveBeenCalledTimes(1);
}),
);

it.effect(
"requires an explicit dot to traverse hidden folders and preserves exact-path errors",
() =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const path = yield* Path.Path;
const cwd = yield* makeTempDir();
yield* writeTextFile(cwd, ".config/projects/index.ts");
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./cfg/", fuzzy: true })).entries,
).toEqual([]);
expect(
(yield* workspaceEntries.browse({ cwd, partialPath: "./.cfg/", fuzzy: true })).entries,
).toEqual([{ name: "projects", fullPath: path.join(cwd, ".config/projects") }]);
const error = yield* workspaceEntries
.browse({ cwd, partialPath: "./cfg/" })
.pipe(Effect.flip);
expect(error._tag).toBe("WorkspaceEntriesReadDirectoryError");
}),
);

it.effect("bounds ambiguous searches instead of scanning every matching subtree", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const cwd = yield* makeTempDir();
for (let index = 0; index < 40; index += 1) {
yield* writeTextFile(cwd, `workspace-${index}/project/index.ts`);
}
vi.mocked(NodeFSP.readdir).mockClear();
const result = yield* workspaceEntries.browse({ cwd, partialPath: "./wrk/", fuzzy: true });
expect(result.entries).toHaveLength(20);
expect(NodeFSP.readdir).toHaveBeenCalledTimes(22);
}),
);

it.effect("returns matching directories and excludes files", () =>
Effect.gen(function* () {
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
Expand Down
Loading
Loading