History

Add VS Code file icons and tighten workspace entry filtering

- render theme-aware VS Code icons for path suggestions in chat composer - exclude `.convex` and gitignored paths more reliably in workspace indexing - add tests plus a sync script and icon manifest/association data

Julius Marminge committed 8 months ago

Commit f04b2d91 parent 3109d1a9 files · +558 −51Browse files

Showing the changes to apps/server/src (2 of 9 files). Show all

Unified · Split

2 files
  1. apps/server/src/workspaceEntries.test.ts +47 −16
  2. apps/server/src/workspaceEntries.ts +74 −12
apps/server/src/workspaceEntries.test.ts+47−16
@@ -3,7 +3,7 @@ import os from "node:os"; · 2 unmodified lines
33 import path from "node:path";
44 import { spawnSync } from "node:child_process";
55
6−import { afterEach, describe, expect, it } from "vitest";
6+import { afterEach, assert, describe, it } from "vitest";
77
88 import { searchWorkspaceEntries } from "./workspaceEntries";
99
@@ -46,13 +46,13 @@ describe("searchWorkspaceEntries", () => { · 36 unmodified lines
4646 const result = await searchWorkspaceEntries({ cwd, query: "", limit: 100 });
4747 const paths = result.entries.map((entry) => entry.path);
4848
49− expect(paths).toContain("src");
50− expect(paths).toContain("src/components");
51− expect(paths).toContain("src/components/Composer.tsx");
52− expect(paths).toContain("README.md");
53− expect(paths.some((entryPath) => entryPath.startsWith(".git"))).toBe(false);
54− expect(paths.some((entryPath) => entryPath.startsWith("node_modules"))).toBe(false);
55− expect(result.truncated).toBe(false);
49+ assert.include(paths, "src");
50+ assert.include(paths, "src/components");
51+ assert.include(paths, "src/components/Composer.tsx");
52+ assert.include(paths, "README.md");
53+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith(".git")));
54+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith("node_modules")));
55+ assert.isFalse(result.truncated);
5656 });
5757
5858 it("filters and ranks entries by query", async () => {
@@ -63,25 +63,56 @@ describe("searchWorkspaceEntries", () => { · 4 unmodified lines
6363
6464 const result = await searchWorkspaceEntries({ cwd, query: "compo", limit: 5 });
6565
66− expect(result.entries.length).toBeGreaterThan(0);
67− expect(result.entries.some((entry) => entry.path === "src/components")).toBe(true);
68− expect(result.entries.every((entry) => entry.path.toLowerCase().includes("compo"))).toBe(true);
66+ assert.isAbove(result.entries.length, 0);
67+ assert.isTrue(result.entries.some((entry) => entry.path === "src/components"));
68+ assert.isTrue(result.entries.every((entry) => entry.path.toLowerCase().includes("compo")));
6969 });
7070
7171 it("excludes gitignored paths for git repositories", async () => {
7272 const cwd = makeTempDir("t3code-workspace-gitignore-");
7373 runGit(cwd, ["init"]);
74− writeFile(cwd, ".gitignore", ".convex/\nignored.txt\n");
74+ writeFile(cwd, ".gitignore", ".convex/\nconvex/\nignored.txt\n");
7575 writeFile(cwd, "src/keep.ts", "export {};");
7676 writeFile(cwd, "ignored.txt", "ignore me");
7777 writeFile(cwd, ".convex/local-storage/data.json", "{}");
78+ writeFile(cwd, "convex/UOoS-l/convex_local_storage/modules/data.json", "{}");
7879
7980 const result = await searchWorkspaceEntries({ cwd, query: "", limit: 100 });
8081 const paths = result.entries.map((entry) => entry.path);
8182
82− expect(paths).toContain("src");
83− expect(paths).toContain("src/keep.ts");
84− expect(paths).not.toContain("ignored.txt");
85− expect(paths.some((entryPath) => entryPath.startsWith(".convex/"))).toBe(false);
83+ assert.include(paths, "src");
84+ assert.include(paths, "src/keep.ts");
85+ assert.notInclude(paths, "ignored.txt");
86+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith(".convex/")));
87+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith("convex/")));
88+ });
89+
90+ it("excludes tracked paths that match ignore rules", async () => {
91+ const cwd = makeTempDir("t3code-workspace-tracked-gitignore-");
92+ runGit(cwd, ["init"]);
93+ writeFile(cwd, ".convex/local-storage/data.json", "{}");
94+ writeFile(cwd, "src/keep.ts", "export {};");
95+ runGit(cwd, ["add", ".convex/local-storage/data.json", "src/keep.ts"]);
96+ writeFile(cwd, ".gitignore", ".convex/\n");
97+
98+ const result = await searchWorkspaceEntries({ cwd, query: "", limit: 100 });
99+ const paths = result.entries.map((entry) => entry.path);
100+
101+ assert.include(paths, "src");
102+ assert.include(paths, "src/keep.ts");
103+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith(".convex/")));
104+ });
105+
106+ it("excludes .convex in non-git workspaces", async () => {
107+ const cwd = makeTempDir("t3code-workspace-non-git-convex-");
108+ writeFile(cwd, ".convex/local-storage/data.json", "{}");
109+ writeFile(cwd, "src/keep.ts", "export {};");
110+
111+ const result = await searchWorkspaceEntries({ cwd, query: "", limit: 100 });
112+ const paths = result.entries.map((entry) => entry.path);
113+
114+ assert.include(paths, "src");
115+ assert.include(paths, "src/keep.ts");
116+ assert.isFalse(paths.some((entryPath) => entryPath.startsWith(".convex/")));
86117 });
87118 });
apps/server/src/workspaceEntries.ts+74−12
@@ -1,4 +1,5 @@
11 import fs from "node:fs/promises";
2+import type { Dirent } from "node:fs";
23 import path from "node:path";
34 import { runProcess } from "./processRunner";
45
@@ -14,6 +15,7 @@ const WORKSPACE_CACHE_MAX_KEYS = 4; · 9 unmodified lines
1415 const WORKSPACE_INDEX_MAX_ENTRIES = 25_000;
1516 const IGNORED_DIRECTORY_NAMES = new Set([
1617 ".git",
18+ ".convex",
1719 "node_modules",
1820 ".next",
1921 ".turbo",
@@ -99,14 +101,52 @@ function directoryAncestorsOf(relativePath: string): string[] { · 79 unmodified lines
99101 return directories;
100102 }
101103
102−async function buildWorkspaceIndexFromGit(cwd: string): Promise<WorkspaceIndex | null> {
104+async function isInsideGitWorkTree(cwd: string): Promise<boolean> {
103105 const insideWorkTree = await runProcess("git", ["rev-parse", "--is-inside-work-tree"], {
104106 cwd,
105107 allowNonZeroExit: true,
106108 timeoutMs: 5_000,
107109 maxBufferBytes: 4_096,
108110 }).catch(() => null);
109− if (!insideWorkTree || insideWorkTree.code !== 0 || insideWorkTree.stdout.trim() !== "true") {
111+ return Boolean(
112+ insideWorkTree && insideWorkTree.code === 0 && insideWorkTree.stdout.trim() === "true",
113+ );
114+}
115+
116+async function filterGitIgnoredPaths(cwd: string, relativePaths: string[]): Promise<string[]> {
117+ if (relativePaths.length === 0) {
118+ return relativePaths;
119+ }
120+
121+ const checkIgnore = await runProcess("git", ["check-ignore", "--no-index", "-z", "--stdin"], {
122+ cwd,
123+ allowNonZeroExit: true,
124+ timeoutMs: 20_000,
125+ maxBufferBytes: 16 * 1024 * 1024,
126+ outputMode: "truncate",
127+ stdin: `${relativePaths.join("\0")}\0`,
128+ }).catch(() => null);
129+ if (!checkIgnore) {
130+ return relativePaths;
131+ }
132+
133+ // git-check-ignore exits with 1 when no paths match.
134+ if (checkIgnore.code !== 0 && checkIgnore.code !== 1) {
135+ return relativePaths;
136+ }
137+
138+ const ignoredPaths = new Set(
139+ splitNullSeparatedPaths(checkIgnore.stdout, Boolean(checkIgnore.stdoutTruncated)),
140+ );
141+ if (ignoredPaths.size === 0) {
142+ return relativePaths;
143+ }
144+
145+ return relativePaths.filter((relativePath) => !ignoredPaths.has(relativePath));
146+}
147+
148+async function buildWorkspaceIndexFromGit(cwd: string): Promise<WorkspaceIndex | null> {
149+ if (!(await isInsideGitWorkTree(cwd))) {
110150 return null;
111151 }
112152
@@ -125,9 +165,13 @@ async function buildWorkspaceIndexFromGit(cwd: string): Promise<WorkspaceIndex | · 12 unmodified lines
125165 return null;
126166 }
127167
128− const filePaths = splitNullSeparatedPaths(listedFiles.stdout, Boolean(listedFiles.stdoutTruncated))
168+ const listedPaths = splitNullSeparatedPaths(
169+ listedFiles.stdout,
170+ Boolean(listedFiles.stdoutTruncated),
171+ )
129172 .map((entry) => toPosixPath(entry))
130173 .filter((entry) => entry.length > 0 && !isPathInIgnoredDirectory(entry));
174+ const filePaths = await filterGitIgnoredPaths(cwd, listedPaths);
131175
132176 const directorySet = new Set<string>();
133177 for (const filePath of filePaths) {
@@ -167,6 +211,7 @@ async function buildWorkspaceIndex(cwd: string): Promise<WorkspaceIndex> { · 33 unmodified lines
167211 if (gitIndexed) {
168212 return gitIndexed;
169213 }
214+ const shouldFilterWithGitIgnore = await isInsideGitWorkTree(cwd);
170215
171216 let pendingDirectories: string[] = [""];
172217 const entries: ProjectEntry[] = [];
@@ -193,13 +238,12 @@ async function buildWorkspaceIndex(cwd: string): Promise<WorkspaceIndex> { · 20 unmodified lines
193238 }),
194239 );
195240
196− for (const directoryEntry of directoryEntries) {
241+ const candidateEntriesByDirectory = directoryEntries.map((directoryEntry) => {
197242 const { relativeDir, dirents } = directoryEntry;
198− if (!dirents) {
199− continue;
200− }
243+ if (!dirents) return [] as Array<{ dirent: Dirent; relativePath: string }>;
201244
202245 dirents.sort((left, right) => left.name.localeCompare(right.name));
246+ const candidates: Array<{ dirent: Dirent; relativePath: string }> = [];
203247 for (const dirent of dirents) {
204248 if (!dirent.name || dirent.name === "." || dirent.name === "..") {
205249 continue;
@@ -217,15 +261,33 @@ async function buildWorkspaceIndex(cwd: string): Promise<WorkspaceIndex> { · 11 unmodified lines
217261 if (isPathInIgnoredDirectory(relativePath)) {
218262 continue;
219263 }
264+ candidates.push({ dirent, relativePath });
265+ }
266+ return candidates;
267+ });
268+
269+ const candidatePaths = candidateEntriesByDirectory.flatMap((candidateEntries) =>
270+ candidateEntries.map((entry) => entry.relativePath),
271+ );
272+ const allowedPathSet = shouldFilterWithGitIgnore
273+ ? new Set(await filterGitIgnoredPaths(cwd, candidatePaths))
274+ : null;
275+
276+ for (const candidateEntries of candidateEntriesByDirectory) {
277+ for (const candidate of candidateEntries) {
278+ if (allowedPathSet && !allowedPathSet.has(candidate.relativePath)) {
279+ continue;
280+ }
281+
220282 const entry: ProjectEntry = {
221− path: relativePath,
222− kind: dirent.isDirectory() ? "directory" : "file",
223− parentPath: parentPathOf(relativePath),
283+ path: candidate.relativePath,
284+ kind: candidate.dirent.isDirectory() ? "directory" : "file",
285+ parentPath: parentPathOf(candidate.relativePath),
224286 };
225287 entries.push(entry);
226288
227− if (dirent.isDirectory()) {
228− pendingDirectories.push(relativePath);
289+ if (candidate.dirent.isDirectory()) {
290+ pendingDirectories.push(candidate.relativePath);
229291 }
230292
231293 if (entries.length >= WORKSPACE_INDEX_MAX_ENTRIES) {