import type { Ignore } from 'ignore' import ignore from 'ignore' /** 读一个工作区文件夹里的东西,路径相对这个文件夹、用 / 分隔。 */ export interface FolderReader { /** 有没有这个文件或目录。 */ exists: (path: string) => Promise /** 文本文件的内容;没有、读不了时没有。 */ read: (path: string) => Promise } /** 和 VS Code 搜索一样读的忽略文件(search.useIgnoreFiles)。 */ const IGNORE_FILES = ['.gitignore', '.ignore'] /** path 所在的各层目录,从工作区文件夹('')往下,不含 path 自己。 */ function directoriesOf(path: string): string[] { const parts = path.split('/').slice(0, -1) return ['', ...parts.map((_, i) => parts.slice(0, i + 1).join('/'))] } function joined(directory: string, name: string): string { return directory ? `${directory}/${name}` : name } /** * 工作区文件夹里建索引时还要跳过的文件(files.exclude、search.exclude 之外): * ① 在自己是一份 checkout 的子目录里(带 `.git` 文件或目录:子仓库、git worktree,例如 `.claude/worktrees/…`)—— * 那是另一份代码的副本,算进来同一个定义会出现两次;工作区文件夹自己的 `.git` 不算。 * ② useIgnoreFiles 开着时(同 VS Code 的 search.useIgnoreFiles,默认开),被路上各层目录的 `.gitignore`、`.ignore` 忽略的: * 每层的规则按 gitignore 写法、相对那一层目录判断;哪一层忽略了就跳过,下层不能再把上层忽略的加回来。 * 工作区文件夹上面的目录、全局的忽略文件、`.git/info/exclude` 不读(VS Code 的 search.useParentIgnoreFiles、 * search.useGlobalIgnoreFiles 默认也不读)。 * 一个实例记着查过的目录,一次扫描、一个文件改动各用一个。 */ export function folderScope(reader: FolderReader, useIgnoreFiles: boolean): { skips: (path: string) => Promise } { const checkouts = new Map>() const rules = new Map>() const isCheckout = (directory: string): Promise => { let found = checkouts.get(directory) if (!found) { found = reader.exists(joined(directory, '.git')) checkouts.set(directory, found) } return found } const rulesIn = (directory: string): Promise => { let found = rules.get(directory) if (!found) { found = Promise.all(IGNORE_FILES.map(name => reader.read(joined(directory, name)))).then((texts) => { const written = texts.filter((text): text is string => text !== undefined) return written.length > 0 ? ignore().add(written.join('\n')) : undefined }) rules.set(directory, found) } return found } return { async skips(path) { const directories = directoriesOf(path) for (const directory of directories.slice(1)) { if (await isCheckout(directory)) return true } if (!useIgnoreFiles) return false for (const directory of directories) { const rule = await rulesIn(directory) if (rule?.ignores(directory ? path.slice(directory.length + 1) : path)) return true } return false }, } }