// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFSP from "node:fs/promises";import * as NodeServices from "@effect/platform-node/NodeServices";import { FileFinder } from "@ff-labs/fff-node";import { it, afterEach, describe, expect } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import { vi } from "vite-plus/test"; import * as ServerConfig from "../config.ts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as VcsProcess from "../vcs/VcsProcess.ts";import * as WorkspaceEntries from "./WorkspaceEntries.ts";import * as WorkspacePaths from "./WorkspacePaths.ts"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof import("node:fs/promises")>(); return { ...actual, readdir: vi.fn(actual.readdir) };}); const TestLayer = Layer.empty.pipe( Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), Layer.provideMerge(WorkspacePaths.layer), Layer.provideMerge(VcsProcess.layer), Layer.provide( ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-workspace-entries-test-", }), ), Layer.provideMerge(NodeServices.layer),); const makeTempDir = Effect.fn(function* (opts?: { prefix?: string; git?: boolean }) { const fileSystem = yield* FileSystem.FileSystem; const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: opts?.prefix ?? "t3code-workspace-entries-", }); if (opts?.git) { yield* git(dir, ["init"]); } return dir;}); function writeTextFile( cwd: string, relativePath: string, contents = "",): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const absolutePath = path.join(cwd, relativePath); yield* fileSystem.makeDirectory(path.dirname(absolutePath), { recursive: true }); yield* fileSystem.writeFileString(absolutePath, contents); });} const git = (cwd: string, args: ReadonlyArray<string>, env?: NodeJS.ProcessEnv) => Effect.gen(function* () { const process = yield* VcsProcess.VcsProcess; const result = yield* process.run({ operation: "WorkspaceEntries.test.git", command: "git", cwd, args, ...(env ? { env } : {}), timeoutMs: 10_000, }); return result.stdout.trim(); }); const searchWorkspaceEntries = (input: { cwd: string; query: string; limit: number; kind?: "file" | "directory";}) => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; return yield* workspaceEntries.search(input); }); const appendSeparator = (input: string) => Effect.map(HostProcessPlatform, (platform) => input.endsWith("/") || input.endsWith("\\") ? input : `${input}${platform === "win32" ? "\\" : "/"}`, ); it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => { afterEach(() => { vi.restoreAllMocks(); }); describe("list", () => { it.effect("returns the complete cached workspace index", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.list({ cwd }); expect(result.entries).toEqual( expect.arrayContaining([ { path: "src", kind: "directory" }, { path: "src/components", kind: "directory" }, { path: "src/components/Composer.tsx", kind: "file", }, { path: "README.md", kind: "file" }, ]), ); expect(result.entries.some((entry) => entry.path.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); }); describe("search", () => { it.effect("returns files and directories relative to cwd", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, ".git/HEAD"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); expect(paths).toContain("README.md"); expect(paths.some((entryPath) => entryPath.startsWith(".git"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); it.effect("filters and ranks entries by query", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "compo", limit: 5 }); expect(result.entries.length).toBeGreaterThan(0); expect(result.entries.some((entry) => entry.path === "src/components")).toBe(true); expect(result.entries.every((entry) => entry.path.toLowerCase().includes("compo"))).toBe( true, ); }), ); it.effect("supports fuzzy subsequence queries for composer path search", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 10 }); const paths = result.entries.map((entry) => entry.path); expect(result.entries.length).toBeGreaterThan(0); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); }), ); it.effect("prioritizes exact basename matches ahead of broader path matches", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-exact-ranking-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "docs/composer.tsx-notes.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "Composer.tsx", limit: 5 }); expect(result.entries[0]?.path).toBe("src/components/Composer.tsx"); }), ); it.effect("tracks truncation without sorting every fuzzy match", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-limit-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 1 }); expect(result.entries).toHaveLength(1); expect(result.truncated).toBe(true); }), ); it.effect("applies the file filter before limiting search results", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-file-limit-" }); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "src/internal.ts"); const result = yield* searchWorkspaceEntries({ cwd, query: "src", limit: 1, kind: "file", }); expect(result.entries).toEqual([{ path: "src/index.ts", kind: "file" }]); expect(result.truncated).toBe(true); }), ); it.effect("answers an empty file-filtered query with a bounded file listing", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-empty-query-" }); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "README.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 10, kind: "file", }); const paths = result.entries.map((entry) => entry.path); expect(paths).toHaveLength(2); expect(paths).toContain("src/index.ts"); expect(paths).toContain("README.md"); expect(result.entries.every((entry) => entry.kind === "file")).toBe(true); }), ); it.effect("returns only directories for the directory filter", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-directory-filter-" }); yield* writeTextFile(cwd, "src/index.ts"); const result = yield* searchWorkspaceEntries({ cwd, query: "src", limit: 10, kind: "directory", }); expect(result.entries).toEqual([{ path: "src", kind: "directory" }]); expect(result.truncated).toBe(false); }), ); it.effect("excludes gitignored paths for git repositories", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-gitignore-", git: true }); yield* writeTextFile(cwd, ".gitignore", ".convex/\nconvex/\nignored.txt\n"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* writeTextFile(cwd, "ignored.txt", "ignore me"); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "convex/UOoS-l/convex_local_storage/modules/data.json", "{}"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths).not.toContain("ignored.txt"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("convex/"))).toBe(false); }), ); it.effect("excludes tracked paths that match ignore rules", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-tracked-gitignore-", git: true, }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* git(cwd, ["add", ".convex/local-storage/data.json", "src/keep.ts"]); yield* writeTextFile(cwd, ".gitignore", ".convex/\n"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("excludes .convex in non-git workspaces", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-non-git-convex-" }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("supports typo-resistant file search through fff", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fff-typo-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); const result = yield* searchWorkspaceEntries({ cwd, query: "compoesr", limit: 10 }); expect(result.entries).toEqual( expect.arrayContaining([ expect.objectContaining({ path: "src/components/Composer.tsx" }), ]), ); }), ); it.effect("rebuilds the cached index after refresh fails", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-refresh-failure-" }); yield* writeTextFile(cwd, "src/index.ts", "export {};\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const createSpy = vi.spyOn(FileFinder, "create"); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(1); vi.spyOn(FileFinder.prototype, "scanFiles").mockReturnValueOnce({ ok: false, error: "scan failed", }); yield* workspaceEntries.refresh(cwd); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(2); }), ); }); describe("searchContents", () => { it.effect("returns content matches with file paths, line numbers, and ranges", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-search-" }); yield* writeTextFile( cwd, "src/shapes.ts", "export const square = 4;\nexport const Square = 16;\nexport const squareSize = 8;\n", ); yield* writeTextFile(cwd, "src/other.ts", "const circle = true;\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "Square", limit: 100, caseSensitive: false, wholeWord: true, useRegex: false, }); expect(result.matches.map((match) => [match.path, match.lineNumber])).toEqual([ ["src/shapes.ts", 1], ["src/shapes.ts", 2], ]); expect(result.matches[0]?.matchRanges).toEqual([{ start: 13, end: 19 }]); expect(result.truncated).toBe(false); }), ); it.effect("honors case sensitivity and gitignore rules", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-ignore-", git: true }); yield* writeTextFile(cwd, ".gitignore", "ignored.txt\n"); yield* writeTextFile(cwd, "src/keep.ts", "square\nSquare\n"); yield* writeTextFile(cwd, "ignored.txt", "Square\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "Square", limit: 100, caseSensitive: true, wholeWord: false, useRegex: false, }); expect(result.matches).toHaveLength(1); expect(result.matches[0]).toMatchObject({ path: "src/keep.ts", lineNumber: 2 }); }), ); it.effect("filters whole-word matches by word boundaries without widening ranges", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-whole-word-" }); yield* writeTextFile(cwd, "src/words.ts", "note notes denote\nfootnote note\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "note", limit: 100, caseSensitive: true, wholeWord: true, useRegex: false, }); // "notes", "denote", and "footnote" are word-adjacent and excluded; // ranges cover exactly the query, never boundary characters. expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 1, matchRanges: [{ start: 0, end: 4 }], }), expect.objectContaining({ path: "src/words.ts", lineNumber: 2, matchRanges: [{ start: 9, end: 13 }], }), ]); }), ); it.effect("finds later whole-word matches in a file after rejected raw matches", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-late-whole-word-" }); yield* writeTextFile(cwd, "src/words.ts", `${"afoo\n".repeat(10)}foo\n`); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "foo", limit: 1, caseSensitive: true, wholeWord: true, useRegex: false, }); expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 11, matchRanges: [{ start: 0, end: 3 }], }), ]); }), ); it.effect("treats astral-plane letters as whole word characters", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-astral-word-" }); yield* writeTextFile(cwd, "src/words.ts", "𐐀foo foo foo𐐀\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "foo", limit: 100, caseSensitive: true, wholeWord: true, useRegex: false, }); expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 1, matchRanges: [{ start: 6, end: 9 }], }), ]); }), ); it.effect("matches punctuation-edged whole-word queries including adjacent occurrences", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-punctuation-" }); yield* writeTextFile(cwd, "src/words.ts", "-foo- -foo- -foo-\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "-foo-", limit: 100, caseSensitive: true,Lines 1–500 of 736. Load more
// @effect-diagnostics nodeBuiltinImport:off import * as NodeFSP from "node:fs/promises"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { FileFinder } from "@ff-labs/fff-node"; import { it, afterEach, describe, expect } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import { vi } from "vite-plus/test"; import * as ServerConfig from "../config.ts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as WorkspaceEntries from "./WorkspaceEntries.ts"; import * as WorkspacePaths from "./WorkspacePaths.ts"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof import("node:fs/promises")>(); return { ...actual, readdir: vi.fn(actual.readdir) }; }); const TestLayer = Layer.empty.pipe( Layer.provideMerge(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), Layer.provideMerge(WorkspacePaths.layer), Layer.provideMerge(VcsProcess.layer), Layer.provide( ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-workspace-entries-test-", }), ), Layer.provideMerge(NodeServices.layer), ); const makeTempDir = Effect.fn(function* (opts?: { prefix?: string; git?: boolean }) { const fileSystem = yield* FileSystem.FileSystem; const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: opts?.prefix ?? "t3code-workspace-entries-", }); if (opts?.git) { yield* git(dir, ["init"]); } return dir; }); function writeTextFile( cwd: string, relativePath: string, contents = "", ): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path> { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const absolutePath = path.join(cwd, relativePath); yield* fileSystem.makeDirectory(path.dirname(absolutePath), { recursive: true }); yield* fileSystem.writeFileString(absolutePath, contents); }); } const git = (cwd: string, args: ReadonlyArray<string>, env?: NodeJS.ProcessEnv) => Effect.gen(function* () { const process = yield* VcsProcess.VcsProcess; const result = yield* process.run({ operation: "WorkspaceEntries.test.git", command: "git", cwd, args, ...(env ? { env } : {}), timeoutMs: 10_000, }); return result.stdout.trim(); }); const searchWorkspaceEntries = (input: { cwd: string; query: string; limit: number; kind?: "file" | "directory"; }) => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; return yield* workspaceEntries.search(input); }); const appendSeparator = (input: string) => Effect.map(HostProcessPlatform, (platform) => input.endsWith("/") || input.endsWith("\\") ? input : `${input}${platform === "win32" ? "\\" : "/"}`, ); it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => { afterEach(() => { vi.restoreAllMocks(); }); describe("list", () => { it.effect("returns the complete cached workspace index", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.list({ cwd }); expect(result.entries).toEqual( expect.arrayContaining([ { path: "src", kind: "directory" }, { path: "src/components", kind: "directory" }, { path: "src/components/Composer.tsx", kind: "file", }, { path: "README.md", kind: "file" }, ]), ); expect(result.entries.some((entry) => entry.path.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); }); describe("search", () => { it.effect("returns files and directories relative to cwd", () => Effect.gen(function* () { const cwd = yield* makeTempDir(); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "README.md"); yield* writeTextFile(cwd, ".git/HEAD"); yield* writeTextFile(cwd, "node_modules/pkg/index.js"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); expect(paths).toContain("README.md"); expect(paths.some((entryPath) => entryPath.startsWith(".git"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("node_modules"))).toBe(false); expect(result.truncated).toBe(false); }), ); it.effect("filters and ranks entries by query", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "compo", limit: 5 }); expect(result.entries.length).toBeGreaterThan(0); expect(result.entries.some((entry) => entry.path === "src/components")).toBe(true); expect(result.entries.every((entry) => entry.path.toLowerCase().includes("compo"))).toBe( true, ); }), ); it.effect("supports fuzzy subsequence queries for composer path search", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-query-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 10 }); const paths = result.entries.map((entry) => entry.path); expect(result.entries.length).toBeGreaterThan(0); expect(paths).toContain("src/components"); expect(paths).toContain("src/components/Composer.tsx"); }), ); it.effect("prioritizes exact basename matches ahead of broader path matches", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-exact-ranking-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "docs/composer.tsx-notes.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "Composer.tsx", limit: 5 }); expect(result.entries[0]?.path).toBe("src/components/Composer.tsx"); }), ); it.effect("tracks truncation without sorting every fuzzy match", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fuzzy-limit-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); yield* writeTextFile(cwd, "src/components/composePrompt.ts"); yield* writeTextFile(cwd, "docs/composition.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "cmp", limit: 1 }); expect(result.entries).toHaveLength(1); expect(result.truncated).toBe(true); }), ); it.effect("applies the file filter before limiting search results", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-file-limit-" }); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "src/internal.ts"); const result = yield* searchWorkspaceEntries({ cwd, query: "src", limit: 1, kind: "file", }); expect(result.entries).toEqual([{ path: "src/index.ts", kind: "file" }]); expect(result.truncated).toBe(true); }), ); it.effect("answers an empty file-filtered query with a bounded file listing", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-empty-query-" }); yield* writeTextFile(cwd, "src/index.ts"); yield* writeTextFile(cwd, "README.md"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 10, kind: "file", }); const paths = result.entries.map((entry) => entry.path); expect(paths).toHaveLength(2); expect(paths).toContain("src/index.ts"); expect(paths).toContain("README.md"); expect(result.entries.every((entry) => entry.kind === "file")).toBe(true); }), ); it.effect("returns only directories for the directory filter", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-directory-filter-" }); yield* writeTextFile(cwd, "src/index.ts"); const result = yield* searchWorkspaceEntries({ cwd, query: "src", limit: 10, kind: "directory", }); expect(result.entries).toEqual([{ path: "src", kind: "directory" }]); expect(result.truncated).toBe(false); }), ); it.effect("excludes gitignored paths for git repositories", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-gitignore-", git: true }); yield* writeTextFile(cwd, ".gitignore", ".convex/\nconvex/\nignored.txt\n"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* writeTextFile(cwd, "ignored.txt", "ignore me"); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "convex/UOoS-l/convex_local_storage/modules/data.json", "{}"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths).not.toContain("ignored.txt"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); expect(paths.some((entryPath) => entryPath.startsWith("convex/"))).toBe(false); }), ); it.effect("excludes tracked paths that match ignore rules", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-tracked-gitignore-", git: true, }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); yield* git(cwd, ["add", ".convex/local-storage/data.json", "src/keep.ts"]); yield* writeTextFile(cwd, ".gitignore", ".convex/\n"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("excludes .convex in non-git workspaces", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-non-git-convex-" }); yield* writeTextFile(cwd, ".convex/local-storage/data.json", "{}"); yield* writeTextFile(cwd, "src/keep.ts", "export {};"); const result = yield* searchWorkspaceEntries({ cwd, query: "", limit: 100 }); const paths = result.entries.map((entry) => entry.path); expect(paths).toContain("src"); expect(paths).toContain("src/keep.ts"); expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false); }), ); it.effect("supports typo-resistant file search through fff", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-fff-typo-" }); yield* writeTextFile(cwd, "src/components/Composer.tsx"); const result = yield* searchWorkspaceEntries({ cwd, query: "compoesr", limit: 10 }); expect(result.entries).toEqual( expect.arrayContaining([ expect.objectContaining({ path: "src/components/Composer.tsx" }), ]), ); }), ); it.effect("rebuilds the cached index after refresh fails", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-refresh-failure-" }); yield* writeTextFile(cwd, "src/index.ts", "export {};\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const createSpy = vi.spyOn(FileFinder, "create"); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(1); vi.spyOn(FileFinder.prototype, "scanFiles").mockReturnValueOnce({ ok: false, error: "scan failed", }); yield* workspaceEntries.refresh(cwd); yield* workspaceEntries.list({ cwd }); expect(createSpy).toHaveBeenCalledTimes(2); }), ); }); describe("searchContents", () => { it.effect("returns content matches with file paths, line numbers, and ranges", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-search-" }); yield* writeTextFile( cwd, "src/shapes.ts", "export const square = 4;\nexport const Square = 16;\nexport const squareSize = 8;\n", ); yield* writeTextFile(cwd, "src/other.ts", "const circle = true;\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "Square", limit: 100, caseSensitive: false, wholeWord: true, useRegex: false, }); expect(result.matches.map((match) => [match.path, match.lineNumber])).toEqual([ ["src/shapes.ts", 1], ["src/shapes.ts", 2], ]); expect(result.matches[0]?.matchRanges).toEqual([{ start: 13, end: 19 }]); expect(result.truncated).toBe(false); }), ); it.effect("honors case sensitivity and gitignore rules", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-ignore-", git: true }); yield* writeTextFile(cwd, ".gitignore", "ignored.txt\n"); yield* writeTextFile(cwd, "src/keep.ts", "square\nSquare\n"); yield* writeTextFile(cwd, "ignored.txt", "Square\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "Square", limit: 100, caseSensitive: true, wholeWord: false, useRegex: false, }); expect(result.matches).toHaveLength(1); expect(result.matches[0]).toMatchObject({ path: "src/keep.ts", lineNumber: 2 }); }), ); it.effect("filters whole-word matches by word boundaries without widening ranges", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-whole-word-" }); yield* writeTextFile(cwd, "src/words.ts", "note notes denote\nfootnote note\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "note", limit: 100, caseSensitive: true, wholeWord: true, useRegex: false, }); // "notes", "denote", and "footnote" are word-adjacent and excluded; // ranges cover exactly the query, never boundary characters. expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 1, matchRanges: [{ start: 0, end: 4 }], }), expect.objectContaining({ path: "src/words.ts", lineNumber: 2, matchRanges: [{ start: 9, end: 13 }], }), ]); }), ); it.effect("finds later whole-word matches in a file after rejected raw matches", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-late-whole-word-" }); yield* writeTextFile(cwd, "src/words.ts", `${"afoo\n".repeat(10)}foo\n`); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "foo", limit: 1, caseSensitive: true, wholeWord: true, useRegex: false, }); expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 11, matchRanges: [{ start: 0, end: 3 }], }), ]); }), ); it.effect("treats astral-plane letters as whole word characters", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-astral-word-" }); yield* writeTextFile(cwd, "src/words.ts", "𐐀foo foo foo𐐀\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "foo", limit: 100, caseSensitive: true, wholeWord: true, useRegex: false, }); expect(result.matches).toEqual([ expect.objectContaining({ path: "src/words.ts", lineNumber: 1, matchRanges: [{ start: 6, end: 9 }], }), ]); }), ); it.effect("matches punctuation-edged whole-word queries including adjacent occurrences", () => Effect.gen(function* () { const cwd = yield* makeTempDir({ prefix: "t3code-workspace-content-punctuation-" }); yield* writeTextFile(cwd, "src/words.ts", "-foo- -foo- -foo-\n"); const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const result = yield* workspaceEntries.searchContents({ cwd, query: "-foo-", limit: 100, caseSensitive: true,