// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFSP from "node:fs/promises"; import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as RcMap from "effect/RcMap";import * as Schema from "effect/Schema"; import type { FilesystemBrowseInput, FilesystemBrowseResult, ProjectListEntriesInput, ProjectListEntriesResult, ProjectSearchContentsInput, ProjectSearchContentsResult, ProjectSearchEntriesInput, ProjectSearchEntriesResult,} from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; import { expandHomePathWith } from "../pathExpansion.ts";import * as WorkspacePaths from "./WorkspacePaths.ts";import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; export class WorkspaceEntriesWindowsPathUnsupportedError extends Schema.TaggedError<WorkspaceEntriesWindowsPathUnsupportedError>()( "WorkspaceEntriesWindowsPathUnsupportedError", { cwd: Schema.optional(Schema.String), partialPath: Schema.String, platform: Schema.String, },) { override get message(): string { const cwd = this.cwd ? ` from '${this.cwd}'` : ""; return `Windows-style workspace path '${this.partialPath}' is not supported on '${this.platform}'${cwd}.`; }} export class WorkspaceEntriesCurrentProjectRequiredError extends Schema.TaggedError<WorkspaceEntriesCurrentProjectRequiredError>()( "WorkspaceEntriesCurrentProjectRequiredError", { partialPath: Schema.String, },) { override get message(): string { return `A current project is required to browse relative workspace path '${this.partialPath}'.`; }} export class WorkspaceEntriesReadDirectoryError extends Schema.TaggedError<WorkspaceEntriesReadDirectoryError>()( "WorkspaceEntriesReadDirectoryError", { cwd: Schema.optional(Schema.String), partialPath: Schema.String, parentPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { const cwd = this.cwd ? ` from '${this.cwd}'` : ""; return `Failed to read workspace directory '${this.parentPath}' while browsing '${this.partialPath}'${cwd}.`; }} export const WorkspaceEntriesBrowseError = Schema.Union([ WorkspaceEntriesWindowsPathUnsupportedError, WorkspaceEntriesCurrentProjectRequiredError, WorkspaceEntriesReadDirectoryError,]);export type WorkspaceEntriesBrowseError = typeof WorkspaceEntriesBrowseError.Type; export const WorkspaceEntriesError = Schema.Union([ WorkspacePaths.WorkspaceRootNotExistsError, WorkspacePaths.WorkspaceRootCreateFailedError, WorkspacePaths.WorkspaceRootStatFailedError, WorkspacePaths.WorkspaceRootNotDirectoryError, WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed, WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut, WorkspaceSearchIndex.WorkspaceSearchIndexSearchFailed,]);export type WorkspaceEntriesError = typeof WorkspaceEntriesError.Type; export class WorkspaceEntries extends Context.Service< WorkspaceEntries, { readonly browse: ( input: FilesystemBrowseInput, ) => Effect.Effect<FilesystemBrowseResult, WorkspaceEntriesBrowseError>; readonly list: ( input: ProjectListEntriesInput, ) => Effect.Effect<ProjectListEntriesResult, WorkspaceEntriesError>; readonly search: ( input: ProjectSearchEntriesInput, ) => Effect.Effect<ProjectSearchEntriesResult, WorkspaceEntriesError>; readonly searchContents: ( input: ProjectSearchContentsInput, ) => Effect.Effect<ProjectSearchContentsResult, WorkspaceEntriesError>; readonly refresh: (cwd: string) => Effect.Effect<void>; }>()("t3/workspace/WorkspaceEntries") {} const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* ( input: FilesystemBrowseInput, path: Path.Path,): Effect.fn.Return<string, WorkspaceEntriesBrowseError> { const platform = yield* HostProcessPlatform; if (platform !== "win32" && isWindowsAbsolutePath(input.partialPath)) { return yield* new WorkspaceEntriesWindowsPathUnsupportedError({ cwd: input.cwd, partialPath: input.partialPath, platform, }); } if (!isExplicitRelativePath(input.partialPath)) { return path.resolve(expandHomePathWith(input.partialPath, path)); } if (!input.cwd) { return yield* new WorkspaceEntriesCurrentProjectRequiredError({ partialPath: input.partialPath, }); } return path.resolve(expandHomePathWith(input.cwd, path), input.partialPath);}); /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.gen(function* () { const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; const workspaceSearchIndexes = yield* WorkspaceSearchIndex.WorkspaceSearchIndexMap; const normalizeWorkspaceRoot = Effect.fn("WorkspaceEntries.normalizeWorkspaceRoot")(function* ( cwd: string, ): Effect.fn.Return<string, WorkspaceEntriesError> { return yield* workspacePaths.normalizeWorkspaceRoot(cwd); }); const refresh: WorkspaceEntries["Service"]["refresh"] = Effect.fn("WorkspaceEntries.refresh")( function* (cwd) { const normalizedCwd = yield* normalizeWorkspaceRoot(cwd).pipe( Effect.orElseSucceed(() => cwd), ); for (const variant of WorkspaceSearchIndex.WORKSPACE_SEARCH_INDEX_VARIANTS) { const indexKey = WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, variant); if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, indexKey))) { continue; } const recoverRefreshFailure = ( cause: | WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed | WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut | WorkspaceSearchIndex.WorkspaceSearchIndexRefreshFailed, ) => Effect.gen(function* () { yield* Effect.logWarning("Failed to refresh workspace search index", { cwd, variant, cause, }); yield* workspaceSearchIndexes.invalidate(indexKey); }); yield* Effect.gen(function* () { const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; yield* searchIndex.refresh(); }).pipe( Effect.provide(workspaceSearchIndexes.get(indexKey)), Effect.catchTags({ WorkspaceSearchIndexCreateFailed: recoverRefreshFailure, WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure, WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure, }), ); } }, ); const browse: WorkspaceEntries["Service"]["browse"] = Effect.fn("WorkspaceEntries.browse")( function* (input) { const resolvedInputPath = yield* resolveBrowseTarget(input, path); const endsWithSeparator = /[\\/]$/.test(input.partialPath) || input.partialPath === "~"; const parentPath = endsWithSeparator ? resolvedInputPath : path.dirname(resolvedInputPath); const prefix = endsWithSeparator ? "" : path.basename(resolvedInputPath); const dirents = yield* Effect.tryPromise({ try: () => NodeFSP.readdir(parentPath, { withFileTypes: true }), catch: (cause) => new WorkspaceEntriesReadDirectoryError({ cwd: input.cwd, partialPath: input.partialPath, parentPath, cause, }), }).pipe( Effect.catchIf( (error) => { const code = (error.cause as NodeJS.ErrnoException | undefined)?.code; return code === "EACCES" || code === "EPERM"; }, () => Effect.succeed([]), ), ); const showHidden = endsWithSeparator || prefix.startsWith("."); const lowerPrefix = prefix.toLowerCase(); const entries: Array<{ readonly name: string; readonly fullPath: string }> = []; for (const dirent of dirents) { if ( dirent.isDirectory() && dirent.name.toLowerCase().startsWith(lowerPrefix) && (showHidden || !dirent.name.startsWith(".")) ) { entries.push({ name: dirent.name, fullPath: path.join(parentPath, dirent.name), }); } } return { parentPath, entries: entries.toSorted((left, right) => left.name.localeCompare(right.name)), }; }, ); const search: WorkspaceEntries["Service"]["search"] = Effect.fn("WorkspaceEntries.search")( function* (input) { const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd); const normalizedQuery = normalizeSearchQuery(input.query, { trimLeadingPattern: /^[@./]+/, }); return yield* Effect.gen(function* () { const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; return yield* searchIndex.search(normalizedQuery, input.limit, input.kind, input.imageOnly); }).pipe( Effect.provide( workspaceSearchIndexes.get( WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "paths"), ), ), ); }, ); const searchContents: WorkspaceEntries["Service"]["searchContents"] = Effect.fn( "WorkspaceEntries.searchContents", )(function* (input) { const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd); return yield* Effect.gen(function* () { const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; return yield* searchIndex.searchContents(input); }).pipe( Effect.provide( workspaceSearchIndexes.get( WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "content"), ), ), ); }); const list: WorkspaceEntries["Service"]["list"] = Effect.fn("WorkspaceEntries.list")( function* (input) { const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd); return yield* Effect.gen(function* () { const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex; return yield* searchIndex.list(); }).pipe( Effect.provide( workspaceSearchIndexes.get( WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "paths"), ), ), ); }, ); return WorkspaceEntries.of({ browse, list, refresh, search, searchContents });}); export const layer = Layer.effect(WorkspaceEntries, make).pipe( Layer.provide(WorkspaceSearchIndex.WorkspaceSearchIndexMap.layer),);
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFSP from "node:fs/promises";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as RcMap from "effect/RcMap";
import * as Schema from "effect/Schema";
import type {
FilesystemBrowseInput,
FilesystemBrowseResult,
ProjectListEntriesInput,
ProjectListEntriesResult,
ProjectSearchContentsInput,
ProjectSearchContentsResult,
ProjectSearchEntriesInput,
ProjectSearchEntriesResult,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
import { expandHomePathWith } from "../pathExpansion.ts";
import * as WorkspacePaths from "./WorkspacePaths.ts";
import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";
export class WorkspaceEntriesWindowsPathUnsupportedError extends Schema.TaggedError<WorkspaceEntriesWindowsPathUnsupportedError>()(
"WorkspaceEntriesWindowsPathUnsupportedError",
{
cwd: Schema.optional(Schema.String),
partialPath: Schema.String,
platform: Schema.String,
},
) {
override get message(): string {
const cwd = this.cwd ? ` from '${this.cwd}'` : "";
return `Windows-style workspace path '${this.partialPath}' is not supported on '${this.platform}'${cwd}.`;
}
}
export class WorkspaceEntriesCurrentProjectRequiredError extends Schema.TaggedError<WorkspaceEntriesCurrentProjectRequiredError>()(
"WorkspaceEntriesCurrentProjectRequiredError",
{
partialPath: Schema.String,
},
) {
override get message(): string {
return `A current project is required to browse relative workspace path '${this.partialPath}'.`;
}
}
export class WorkspaceEntriesReadDirectoryError extends Schema.TaggedError<WorkspaceEntriesReadDirectoryError>()(
"WorkspaceEntriesReadDirectoryError",
{
cwd: Schema.optional(Schema.String),
partialPath: Schema.String,
parentPath: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
const cwd = this.cwd ? ` from '${this.cwd}'` : "";
return `Failed to read workspace directory '${this.parentPath}' while browsing '${this.partialPath}'${cwd}.`;
}
}
export const WorkspaceEntriesBrowseError = Schema.Union([
WorkspaceEntriesWindowsPathUnsupportedError,
WorkspaceEntriesCurrentProjectRequiredError,
WorkspaceEntriesReadDirectoryError,
]);
export type WorkspaceEntriesBrowseError = typeof WorkspaceEntriesBrowseError.Type;
export const WorkspaceEntriesError = Schema.Union([
WorkspacePaths.WorkspaceRootNotExistsError,
WorkspacePaths.WorkspaceRootCreateFailedError,
WorkspacePaths.WorkspaceRootStatFailedError,
WorkspacePaths.WorkspaceRootNotDirectoryError,
WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed,
WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut,
WorkspaceSearchIndex.WorkspaceSearchIndexSearchFailed,
]);
export type WorkspaceEntriesError = typeof WorkspaceEntriesError.Type;
export class WorkspaceEntries extends Context.Service<
WorkspaceEntries,
{
readonly browse: (
input: FilesystemBrowseInput,
) => Effect.Effect<FilesystemBrowseResult, WorkspaceEntriesBrowseError>;
readonly list: (
input: ProjectListEntriesInput,
) => Effect.Effect<ProjectListEntriesResult, WorkspaceEntriesError>;
readonly search: (
input: ProjectSearchEntriesInput,
) => Effect.Effect<ProjectSearchEntriesResult, WorkspaceEntriesError>;
readonly searchContents: (
input: ProjectSearchContentsInput,
) => Effect.Effect<ProjectSearchContentsResult, WorkspaceEntriesError>;
readonly refresh: (cwd: string) => Effect.Effect<void>;
}
>()("t3/workspace/WorkspaceEntries") {}
const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* (
input: FilesystemBrowseInput,
path: Path.Path,
): Effect.fn.Return<string, WorkspaceEntriesBrowseError> {
const platform = yield* HostProcessPlatform;
if (platform !== "win32" && isWindowsAbsolutePath(input.partialPath)) {
return yield* new WorkspaceEntriesWindowsPathUnsupportedError({
cwd: input.cwd,
partialPath: input.partialPath,
platform,
});
}
if (!isExplicitRelativePath(input.partialPath)) {
return path.resolve(expandHomePathWith(input.partialPath, path));
}
if (!input.cwd) {
return yield* new WorkspaceEntriesCurrentProjectRequiredError({
partialPath: input.partialPath,
});
}
return path.resolve(expandHomePathWith(input.cwd, path), input.partialPath);
});
/** @public Service construction is part of the canonical Effect module API. */
export const make = Effect.gen(function* () {
const path = yield* Path.Path;
const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
const workspaceSearchIndexes = yield* WorkspaceSearchIndex.WorkspaceSearchIndexMap;
const normalizeWorkspaceRoot = Effect.fn("WorkspaceEntries.normalizeWorkspaceRoot")(function* (
cwd: string,
): Effect.fn.Return<string, WorkspaceEntriesError> {
return yield* workspacePaths.normalizeWorkspaceRoot(cwd);
});
const refresh: WorkspaceEntries["Service"]["refresh"] = Effect.fn("WorkspaceEntries.refresh")(
function* (cwd) {
const normalizedCwd = yield* normalizeWorkspaceRoot(cwd).pipe(
Effect.orElseSucceed(() => cwd),
);
for (const variant of WorkspaceSearchIndex.WORKSPACE_SEARCH_INDEX_VARIANTS) {
const indexKey = WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, variant);
if (!(yield* RcMap.has(workspaceSearchIndexes.rcMap, indexKey))) {
continue;
}
const recoverRefreshFailure = (
cause:
| WorkspaceSearchIndex.WorkspaceSearchIndexCreateFailed
| WorkspaceSearchIndex.WorkspaceSearchIndexScanTimedOut
| WorkspaceSearchIndex.WorkspaceSearchIndexRefreshFailed,
) =>
Effect.gen(function* () {
yield* Effect.logWarning("Failed to refresh workspace search index", {
cwd,
variant,
cause,
});
yield* workspaceSearchIndexes.invalidate(indexKey);
});
yield* Effect.gen(function* () {
const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
yield* searchIndex.refresh();
}).pipe(
Effect.provide(workspaceSearchIndexes.get(indexKey)),
Effect.catchTags({
WorkspaceSearchIndexCreateFailed: recoverRefreshFailure,
WorkspaceSearchIndexScanTimedOut: recoverRefreshFailure,
WorkspaceSearchIndexRefreshFailed: recoverRefreshFailure,
}),
);
}
},
);
const browse: WorkspaceEntries["Service"]["browse"] = Effect.fn("WorkspaceEntries.browse")(
function* (input) {
const resolvedInputPath = yield* resolveBrowseTarget(input, path);
const endsWithSeparator = /[\\/]$/.test(input.partialPath) || input.partialPath === "~";
const parentPath = endsWithSeparator ? resolvedInputPath : path.dirname(resolvedInputPath);
const prefix = endsWithSeparator ? "" : path.basename(resolvedInputPath);
const dirents = yield* Effect.tryPromise({
try: () => NodeFSP.readdir(parentPath, { withFileTypes: true }),
catch: (cause) =>
new WorkspaceEntriesReadDirectoryError({
cwd: input.cwd,
partialPath: input.partialPath,
parentPath,
cause,
}),
}).pipe(
Effect.catchIf(
(error) => {
const code = (error.cause as NodeJS.ErrnoException | undefined)?.code;
return code === "EACCES" || code === "EPERM";
},
() => Effect.succeed([]),
),
);
const showHidden = endsWithSeparator || prefix.startsWith(".");
const lowerPrefix = prefix.toLowerCase();
const entries: Array<{ readonly name: string; readonly fullPath: string }> = [];
for (const dirent of dirents) {
if (
dirent.isDirectory() &&
dirent.name.toLowerCase().startsWith(lowerPrefix) &&
(showHidden || !dirent.name.startsWith("."))
) {
entries.push({
name: dirent.name,
fullPath: path.join(parentPath, dirent.name),
});
}
}
return {
parentPath,
entries: entries.toSorted((left, right) => left.name.localeCompare(right.name)),
};
},
);
const search: WorkspaceEntries["Service"]["search"] = Effect.fn("WorkspaceEntries.search")(
function* (input) {
const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd);
const normalizedQuery = normalizeSearchQuery(input.query, {
trimLeadingPattern: /^[@./]+/,
});
return yield* Effect.gen(function* () {
const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
return yield* searchIndex.search(normalizedQuery, input.limit, input.kind, input.imageOnly);
}).pipe(
Effect.provide(
workspaceSearchIndexes.get(
WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "paths"),
),
),
);
},
);
const searchContents: WorkspaceEntries["Service"]["searchContents"] = Effect.fn(
"WorkspaceEntries.searchContents",
)(function* (input) {
const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd);
return yield* Effect.gen(function* () {
const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
return yield* searchIndex.searchContents(input);
}).pipe(
Effect.provide(
workspaceSearchIndexes.get(
WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "content"),
),
),
);
});
const list: WorkspaceEntries["Service"]["list"] = Effect.fn("WorkspaceEntries.list")(
function* (input) {
const normalizedCwd = yield* normalizeWorkspaceRoot(input.cwd);
return yield* Effect.gen(function* () {
const searchIndex = yield* WorkspaceSearchIndex.WorkspaceSearchIndex;
return yield* searchIndex.list();
}).pipe(
Effect.provide(
workspaceSearchIndexes.get(
WorkspaceSearchIndex.workspaceSearchIndexKey(normalizedCwd, "paths"),
),
),
);
},
);
return WorkspaceEntries.of({ browse, list, refresh, search, searchContents });
});
export const layer = Layer.effect(WorkspaceEntries, make).pipe(
Layer.provide(WorkspaceSearchIndex.WorkspaceSearchIndexMap.layer),
);