feat(file-icons): FileIconDescriptor 支持 names / folderNames 与 open 参数

图标主题(VSCode 的 fileNames / folderNames 半边)需要按**文件名**和**目录名**
着色,扩展名规则表达不了 `package.json` / `Dockerfile` / `node_modules`。

- `names?: readonly string[]`:精确 basename(大小写不敏感),命中优先于扩展名。
- `folderNames?: readonly string[]`:精确目录 basename,命中优先于保留扩展名;
  只认领列出的目录,未列出的目录仍走 `'folder'`/`'folder-open'` 或内置 glyph。
- `exts` 变为可选:省略 = 没有扩展名规则(`names`-only 注册不再是 catch-all);
  `exts: []` 仍是全局默认 catch-all,语义不变。
- `icon(path, size, open?)`:目录行的展开态传入工厂,一条注册即可渲染开/合两态。
- `matchFileIcon` 先 names 后 exts;`matchFolderIcon(open, name?)` 先 folderNames
  后保留扩展名(新参数可选,旧调用不变)。
- 指南 §7 同步:字段、匹配顺序、四例注册(names / folderNames / 全目录 / catch-all)。

测试:`tests/file-icons.spec.tsx` 19 → 23(names 大小写、name 优先于 ext、
folderNames 只认领列出目录、open 传入工厂);`tests/consumer-types.ts` 补新字段。
This commit is contained in:
Menghuan1918
2026-09-08 21:24:03 +08:00
parent 8722bfb29c
commit a9a635138a
4 changed files with 186 additions and 63 deletions
+5 -2
View File
@@ -132,9 +132,11 @@ service.openFile({ sessionId: 's1', cwd: '/p' }, '/p/a.csv', 'Data')
const icon: FileIconDescriptor = {
id: 'my-plugin:icons',
exts: ['csv', FOLDER_EXT, FOLDER_OPEN_EXT],
names: ['package.json'],
folderNames: ['node_modules'],
priority: 5,
icon: (path: string, size: number) => {
void path; void size
icon: (path: string, size: number, open?: boolean) => {
void path; void size; void open
return null
},
}
@@ -142,6 +144,7 @@ service.registerFileIcon(icon)
service.getFileIcons()
service.matchFileIcon('/p/a.csv')
service.matchFolderIcon(true)
service.matchFolderIcon(true, 'node_modules')
service.fileIcon('/p/a.csv', 14)
service.folderIcon('/p', true, 14)
+51
View File
@@ -172,6 +172,57 @@ describe('fileIcon resolver chain (specific → builtin → catch-all → VscFil
})
})
describe('name matching (names / folderNames — the icon-theme half)', () => {
it('a names rule claims the exact basename, case-insensitively', () => {
const service = createBetterSidebarService(createSidebarStore())
service.registerFileIcon({ id: 'npm', names: ['package.json'], icon: marker })
expect(service.matchFileIcon('/w/package.json')?.id).toBe('npm')
expect(service.matchFileIcon('/w/Package.JSON')?.id).toBe('npm')
expect(service.matchFileIcon('/w/pkg.json')).toBeUndefined()
})
it('a name rule outranks an extension rule and the built-in glyph', () => {
const service = createBetterSidebarService(createSidebarStore())
service.registerFileIcon({ id: 'by-ext', exts: ['json'], icon: markerB })
service.registerFileIcon({ id: 'by-name', names: ['package.json'], icon: marker })
expect(service.matchFileIcon('/w/package.json')?.id).toBe('by-name')
expect(glyphOf(service.fileIcon('/w/package.json', 14))).toBe('span')
// Other json files still take the extension rule.
expect(glyphOf(service.fileIcon('/w/tsconfig.json', 14))).toBe('b')
})
it('a folderNames rule claims only the directories it names, and outranks the reserved exts', () => {
const service = createBetterSidebarService(createSidebarStore())
service.registerFileIcon({ id: 'all-dirs', exts: ['folder', 'folder-open'], icon: markerB })
service.registerFileIcon({ id: 'named', folderNames: ['node_modules'], icon: marker })
expect(service.matchFolderIcon(false, 'node_modules')?.id).toBe('named')
expect(service.matchFolderIcon(false, 'NODE_MODULES')?.id).toBe('named')
// An unnamed directory still takes the reserved-ext registration.
expect(service.matchFolderIcon(false, 'src')?.id).toBe('all-dirs')
// A descriptor with folderNames only never claims unnamed directories.
const solo = createBetterSidebarService(createSidebarStore())
solo.registerFileIcon({ id: 'named', folderNames: ['src'], icon: marker })
expect(solo.matchFolderIcon(false, 'lib')).toBeUndefined()
expect(glyphOf(solo.folderIcon('/w/lib', false, 14))).toBe(VscFolder)
})
it('the folder factory receives the open flag so one descriptor renders both states', () => {
const service = createBetterSidebarService(createSidebarStore())
const seen: (boolean | undefined)[] = []
service.registerFileIcon({
id: 'stateful',
folderNames: ['src'],
icon: (_path, _size, open) => {
seen.push(open)
return open === true ? marker() : markerB()
},
})
expect(glyphOf(service.folderIcon('/w/src', false, 14))).toBe('b')
expect(glyphOf(service.folderIcon('/w/src', true, 14))).toBe('span')
expect(seen).toEqual([false, true])
})
})
describe('folderIcon resolver (registered folder/folder-open → builtin glyphs)', () => {
it('unregistered directories show the builtin glyphs', () => {
const service = createBetterSidebarService(createSidebarStore())