Files
superwork/vscode/src/codeReader/workspaceScope.ts
T

78 lines
3.2 KiB
TypeScript

import type { Ignore } from 'ignore'
import ignore from 'ignore'
/** 读一个工作区文件夹里的东西,路径相对这个文件夹、用 / 分隔。 */
export interface FolderReader {
/** 有没有这个文件或目录。 */
exists: (path: string) => Promise<boolean>
/** 文本文件的内容;没有、读不了时没有。 */
read: (path: string) => Promise<string | undefined>
}
/** 和 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<boolean> } {
const checkouts = new Map<string, Promise<boolean>>()
const rules = new Map<string, Promise<Ignore | undefined>>()
const isCheckout = (directory: string): Promise<boolean> => {
let found = checkouts.get(directory)
if (!found) {
found = reader.exists(joined(directory, '.git'))
checkouts.set(directory, found)
}
return found
}
const rulesIn = (directory: string): Promise<Ignore | undefined> => {
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
},
}
}