Skip to content

Commit

Permalink
fix(vscode): exclude ignored files for chat panel at file list. (#3765)
Browse files Browse the repository at this point in the history
* fix(vscode): exclude ignored files for chat panel @file list.

* fix: handle emplty gitignore line.

* fix: typo in comments.
  • Loading branch information
icycodes authored Jan 27, 2025
1 parent 78fd2e5 commit 8897506
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 3 deletions.
5 changes: 2 additions & 3 deletions clients/vscode/src/chat/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
localUriToListFileItem,
escapeGlobPattern,
} from "./utils";
import { findFiles } from "../findFiles";
import mainHtml from "./html/main.html";
import errorHtml from "./html/error.html";

Expand Down Expand Up @@ -529,10 +530,8 @@ export class ChatWebview {
.join("");

const globPattern = `**/${caseInsensitivePattern}*`;

this.logger.info(`Searching files with pattern: ${globPattern}, limit: ${maxResults}`);

const files = await workspace.findFiles(globPattern, undefined, maxResults);
const files = await findFiles(globPattern, { maxResults });
this.logger.info(`Found ${files.length} files.`);
return files.map((uri) => localUriToListFileItem(uri, this.gitProvider));
} catch (error) {
Expand Down
183 changes: 183 additions & 0 deletions clients/vscode/src/findFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// We want to use the findFiles2 API, but it is still in the proposed state.
// Therefore, we implement a findFiles function here that can use the following rules in the exclude pattern:
// 1. .gitignore files
// 2. Settings from `files.exclude` and `search.exclude`
//
// See https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts

import { GlobPattern, RelativePattern, Uri, WorkspaceFolder, CancellationToken, workspace } from "vscode";
import path from "path";
import { getLogger } from "./logger";

const logger = getLogger("FindFiles");

// Map from workspace folder to gitignore patterns
const gitIgnorePatternsMap = new Map<string, Set<string>>();

function gitIgnoreItemToExcludePatterns(item: string, prefix?: string | undefined): string[] {
let pattern = item.trim();
if (pattern.length === 0) {
return [];
}
if (pattern.indexOf("/") === -1 || pattern.indexOf("/") === pattern.length - 1) {
if (!pattern.startsWith("**/")) {
pattern = `**/${pattern}`;
}
} else if (pattern.startsWith("/")) {
pattern = pattern.slice(1);
}
return [path.join(prefix ?? "", pattern), path.join(prefix ?? "", pattern, "/**")];
}

async function buildGitIgnorePatterns(workspaceFolder: WorkspaceFolder) {
const patterns = new Set<string>();
logger.debug(`Building gitignore patterns for workspace folder: ${workspaceFolder.uri.toString()}`);

// Read parent gitignore files
let current = workspaceFolder.uri;
let parent = current.with({ path: path.dirname(current.path) });
while (parent.path !== current.path) {
const gitignore = parent.with({ path: path.join(parent.path, ".gitignore") });
try {
const content = (await workspace.fs.readFile(gitignore)).toString();
content.split(/\r?\n/).forEach((line) => {
if (!line.trim().startsWith("#")) {
gitIgnoreItemToExcludePatterns(line).forEach((pattern) => patterns.add(pattern));
}
});
} catch (error) {
// ignore
}

// next
current = parent;
parent = current.with({ path: path.dirname(current.path) });
}

// Read subdirectories gitignore files
const ignoreFiles = await workspace.findFiles(new RelativePattern(workspaceFolder, "**/.gitignore"));
await Promise.all(
ignoreFiles.map(async (ignoreFile) => {
const prefix = path.relative(workspaceFolder.uri.path, path.dirname(ignoreFile.path));
try {
const content = (await workspace.fs.readFile(ignoreFile)).toString();
content.split(/\r?\n/).forEach((line) => {
if (!line.trim().startsWith("#")) {
gitIgnoreItemToExcludePatterns(line, prefix).forEach((pattern) => patterns.add(pattern));
}
});
} catch (error) {
// ignore
}
}),
);
// Update map
logger.debug(
`Completed building git ignore patterns for workspace folder: ${workspaceFolder.uri.toString()}, git ignore patterns: ${JSON.stringify([...patterns])}`,
);
gitIgnorePatternsMap.set(workspaceFolder.uri.toString(), patterns);
}

workspace.onDidChangeTextDocument(async (event) => {
const uri = event.document.uri;
if (path.basename(uri.fsPath) === ".gitignore") {
const workspaceFolder = workspace.getWorkspaceFolder(uri);
if (workspaceFolder) {
await buildGitIgnorePatterns(workspaceFolder);
}
}
});

export async function findFiles(
pattern: GlobPattern,
options?: {
excludes?: string[];
noUserSettings?: boolean; // User settings is used by default, set to true to skip user settings
noIgnoreFiles?: boolean; // .gitignore files are used by default, set to true to skip .gitignore files
maxResults?: number;
token?: CancellationToken;
},
): Promise<Uri[]> {
const combinedExcludes = new Set<string>();
if (options?.excludes) {
for (const exclude of options.excludes) {
combinedExcludes.add(exclude);
}
}
if (!options?.noUserSettings) {
const searchExclude: Record<string, boolean> =
(await workspace.getConfiguration("search", null).get("exclude")) ?? {};
const filesExclude: Record<string, boolean> =
(await workspace.getConfiguration("files", null).get("exclude")) ?? {};
for (const pattern in { ...searchExclude, ...filesExclude }) {
if (filesExclude[pattern]) {
combinedExcludes.add(pattern);
}
}
}
if (options?.noIgnoreFiles) {
const excludesPattern = `{${[...combinedExcludes].join(",")}}`;
logger.debug(
`Executing search: ${JSON.stringify({ includePattern: pattern, excludesPattern, maxResults: options?.maxResults, token: options?.token })}`,
);
return await workspace.findFiles(pattern, excludesPattern, options.maxResults, options.token);
} else {
await Promise.all(
workspace.workspaceFolders?.map(async (workspaceFolder) => {
if (!gitIgnorePatternsMap.has(workspaceFolder.uri.toString())) {
await buildGitIgnorePatterns(workspaceFolder);
}
}) ?? [],
);

return new Promise((resolve, reject) => {
if (options?.token) {
options?.token.onCancellationRequested((reason) => {
reject(reason);
});
}

const allSearches =
workspace.workspaceFolders?.map(async (workspaceFolder) => {
let includePattern: RelativePattern;
if (typeof pattern === "string") {
includePattern = new RelativePattern(workspaceFolder, pattern);
} else {
if (pattern.baseUri.toString().startsWith(workspaceFolder.uri.toString())) {
includePattern = pattern;
} else {
return [];
}
}
const allExcludes = new Set([
...combinedExcludes,
...(gitIgnorePatternsMap.get(workspaceFolder.uri.toString()) ?? []),
]);
const excludesPattern = `{${[...allExcludes].slice(0, 1000).join(",")}}`; // Limit to 1000 patterns
logger.debug(
`Executing search: ${JSON.stringify({ includePattern, excludesPattern, maxResults: options?.maxResults, token: options?.token })}`,
);
return await workspace.findFiles(includePattern, excludesPattern, options?.maxResults, options?.token);
}) ?? [];

const results: Uri[] = [];
Promise.all(
allSearches.map(async (search) => {
try {
const result = await search;
if (result.length > 0) {
results.push(...result);
if (options?.maxResults && results.length >= options.maxResults) {
resolve(results.slice(0, options.maxResults));
}
}
} catch (error) {
// ignore
}
}),
).then(() => {
resolve(results);
});
});
}
}

0 comments on commit 8897506

Please sign in to comment.