fix(explorer): 文件名搜索跳过 node_modules 等噪声目录
右侧文件窗口的全局文件名搜索会深度优先遍历整个工作区,原先只跳过 .git。含大型 node_modules 的仓库会先把 10 万条访问预算耗尽,导致 docs/ 等后序目录里的真实文件搜不到。 搜索改为跳过依赖、包管理存储和常见构建缓存目录(不匹配也不下降), 并补回归测试钉住 node_modules / dist 内的命中不会泄漏。
This commit is contained in:
@@ -712,7 +712,7 @@ better-sidebar 自己的内置 tab 和 viewer 就是参考实现("吃狗粮"
|
||||
- **`tests/service.spec.ts`**:注册表生命周期 / 匹配算法 / dedupe / createTab / 启用态 gating 测试
|
||||
- **`tests/builtins.spec.ts`**:内置注册清单断言(7 tab + 6 viewer + 声明式元数据)
|
||||
- **`src/client/plugins-tabs.ts`** / **`src/client/plugins-viewers.ts`**:推荐插件目录(名字/url/简介/安装脚本,分别对应 Tab 注册与文件预览注册),在设置页两个「添加插件」弹窗展示(共享类型在 `plugins-shared.ts`);插件作者可按扩展点加一条数据(弹窗内「跳转」直达仓库、「复制」把安装命令写入剪贴板,粘贴到 DSH 所在环境的终端执行)——数据完整性由 `tests/plugin-list.spec.ts` 守护
|
||||
- **`src/client/FileTree.tsx`** / **`src/client/TreePanel.tsx`** / **`src/fs-search.ts`**:受控文件树组件(纯树体,文件行右键菜单含「在新 Tab 中打开」「在侧边打开」,仅宿主编排提供回调时渲染)/ 树面板(搜索框 + 刷新 + FileTree,文件窗口的内嵌 dock 使用)与 host 侧递归文件名搜索(`fs.search` 路由,预算兜底 + 跳过 `.git`/symlink 目录;测试 `tests/fs-search.spec.ts`、组件测试 `tests/editor-host.spec.tsx`)
|
||||
- **`src/client/FileTree.tsx`** / **`src/client/TreePanel.tsx`** / **`src/fs-search.ts`**:受控文件树组件(纯树体,文件行右键菜单含「在新 Tab 中打开」「在侧边打开」,仅宿主编排提供回调时渲染)/ 树面板(搜索框 + 刷新 + FileTree,文件窗口的内嵌 dock 使用)与 host 侧递归文件名搜索(`fs.search` 路由,预算兜底 + 跳过 `.git` / `node_modules` / 构建缓存 / symlink 目录;测试 `tests/fs-search.spec.ts`、组件测试 `tests/editor-host.spec.tsx`)
|
||||
- **`docs/plans/2026-08-11-service-registry-design.md`** / **`docs/plans/2026-08-11-declarative-sidebar-settings-design.md`** / **`docs/plans/2026-08-14-add-plugins-modal-design.md`**:设计文档(含实施偏差记录)
|
||||
|
||||
调试时直接读这些文件即可看到所有 API 的真实用法。
|
||||
|
||||
+35
-8
@@ -3,14 +3,14 @@
|
||||
* Streams the tree with opendir and matches the query as a case-insensitive
|
||||
* substring of each entry's NAME (paths stay relative to the search root —
|
||||
* the client resolves them against the session cwd). No .gitignore semantics
|
||||
* (this is a name lookup, not a code search), but `.git` directories are
|
||||
* skipped outright (VCS internals are never useful results) and symlink
|
||||
* directories are NOT descended (cycle safety).
|
||||
* (this is a name lookup, not a code search), but known noise directories
|
||||
* (`.git`, `node_modules`, package-manager stores, build caches) are
|
||||
* skipped outright and symlink directories are NOT descended (cycle safety).
|
||||
*
|
||||
* Two performance budgets bound the walk: `maxMatches` (the client renders
|
||||
* the flat list) and `maxVisited` (a runaway tree — a home directory root,
|
||||
* a node_modules forest — must not stall the host). Exceeding either stops
|
||||
* early with `truncated: true`.
|
||||
* the flat list) and `maxVisited` (a runaway tree — a home directory root
|
||||
* — must not stall the host). Exceeding either stops early with
|
||||
* `truncated: true`.
|
||||
*/
|
||||
import { opendir } from 'node:fs/promises'
|
||||
import { join, relative, sep } from 'node:path'
|
||||
@@ -33,6 +33,33 @@ export interface FsSearchOptions {
|
||||
const DEFAULT_MAX_MATCHES = 200
|
||||
const DEFAULT_MAX_VISITED = 100_000
|
||||
|
||||
/**
|
||||
* Directory names that are never useful filename-search results and would
|
||||
* burn the visit budget before the walk reaches project files. Compared
|
||||
* case-insensitively so `Node_Modules` / `.GIT` stay skipped on every
|
||||
* platform. The directory itself is neither matched nor descended.
|
||||
*/
|
||||
const SEARCH_SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
'.pnpm-store',
|
||||
'.yarn',
|
||||
'.turbo',
|
||||
'.turbopack',
|
||||
'.next',
|
||||
'.nuxt',
|
||||
'.output',
|
||||
'.cache',
|
||||
'.parcel-cache',
|
||||
'coverage',
|
||||
'dist',
|
||||
'build',
|
||||
'out',
|
||||
'.umi',
|
||||
'.umi-production',
|
||||
'.dumi',
|
||||
])
|
||||
|
||||
/**
|
||||
* Search `root` recursively for entries whose name contains `query`
|
||||
* (case-insensitive).
|
||||
@@ -63,8 +90,8 @@ export async function searchFiles(root: string, query: string, opts: FsSearchOpt
|
||||
truncated = true
|
||||
return
|
||||
}
|
||||
// .git is VCS-internal noise: never matched, never descended.
|
||||
if (dirent.isDirectory() && dirent.name === '.git') continue
|
||||
// Dependency / VCS / build-output forests: never matched, never descended.
|
||||
if (dirent.isDirectory() && SEARCH_SKIP_DIRS.has(dirent.name.toLowerCase())) continue
|
||||
if (dirent.name.toLowerCase().includes(needle)) {
|
||||
matches.push(join(relative(root, dir), dirent.name))
|
||||
if (matches.length >= maxMatches) {
|
||||
|
||||
+25
-3
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* fs-search: the host's recursive file-name search behind the editor side
|
||||
* panel's search box. Matches are case-insensitive name substrings, reported
|
||||
* RELATIVE to the root ('/'-separated); `.git` directories are skipped,
|
||||
* symlinked directories are never descended (cycle safety), and the
|
||||
* maxMatches/maxVisited budgets stop a runaway walk with `truncated: true`.
|
||||
* RELATIVE to the root ('/'-separated); noise directories (`.git`,
|
||||
* `node_modules`, build caches) are skipped, symlinked directories are
|
||||
* never descended (cycle safety), and the maxMatches/maxVisited budgets
|
||||
* stop a runaway walk with `truncated: true`.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
@@ -86,6 +87,27 @@ describe('fs-search', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('never descends into node_modules or other noise directories', async () => {
|
||||
const dir = makeFixture()
|
||||
try {
|
||||
mkdirSync(join(dir, 'node_modules', 'left-pad'), { recursive: true })
|
||||
mkdirSync(join(dir, 'web', 'dist'), { recursive: true })
|
||||
writeFileSync(join(dir, 'node_modules', 'left-pad', 'guide.md'), 'dep')
|
||||
writeFileSync(join(dir, 'web', 'dist', 'bundle.js'), 'build')
|
||||
writeFileSync(join(dir, 'web', 'app.ts'), 'src')
|
||||
// A match hidden behind node_modules / dist must not appear; project
|
||||
// files after those forests must still be reachable within budget.
|
||||
expect((await searchFiles(dir, 'guide')).matches).toEqual(['docs/guide.md'])
|
||||
expect((await searchFiles(dir, 'left-pad')).matches).toEqual([])
|
||||
expect((await searchFiles(dir, 'bundle')).matches).toEqual([])
|
||||
expect((await searchFiles(dir, 'app.ts')).matches).toEqual(['web/app.ts'])
|
||||
expect((await searchFiles(dir, 'node_modules')).matches).toEqual([])
|
||||
expect((await searchFiles(dir, 'dist')).matches).toEqual([])
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('an empty (or whitespace) query matches nothing without walking', async () => {
|
||||
const dir = makeFixture()
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user