✨ feat(vscode): 代码阅读侧栏加「未提交的更改」筛选

- 标题栏新按钮「未提交的更改」:树里只剩工作区各 git 仓库里没提交的文件(暂存、没暂存、未跟踪、合并冲突),每个有改动的仓库一个根,跟着 git 状态自动重摆;上色和徽标交给 VS Code 自带的 git 装饰
- 两种筛选互斥,由 TreeFilters 统一管,上下文键合并成 superpowers.codeReader.filter(review / uncommitted)
- 改动文件树改成多根,buildChangedTree 挪到 explorer/changedTree 并改吃 {path, change}
- 内置 git 扩展 API 的类型和加载抽到 src/git/gitApi、gitExtension,提交按钮的监听也改用它
- 文件注释按文件所在的工作区文件夹记,不看树从哪个根摆下来
This commit is contained in:
cruldra committed 2026-09-29 23:42:10 +08:00
1 parent 61c4728dce
commit 7f460d1257
18 files changed
+847 -240

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { buildChangedTree, CHANGE_STYLES, changeOf } from './changedFiles'
import { CHANGE_STYLES, changeOf } from './changedFiles'
describe('改动状态', () => {
it('新增 A、删除 D,别的(改名、复制、改了、没改)都算修改 M', () => {
@@ -16,65 +16,3 @@ describe('改动状态', () => {
expect(CHANGE_STYLES.D.tooltip).toBe('PR 里删掉了')
})
})
describe('改动的文件树', () => {
// 原型「审查 · 改动的文件」里的 6 个文件,外加一个改名的
const files = [
{ path: 'backend/src/luffy_agent/user/routes.py', status: 'changed' },
{ path: 'backend/tests/user/test_avatar.py', status: 'added' },
{ path: 'backend/src/luffy_agent/core/storage.py', status: 'changed' },
{ path: 'backend/src/luffy_agent/user/legacy_profile.py', status: 'deleted' },
{ path: 'backend/src/luffy_agent/user/services.py', status: 'changed' },
{ path: 'backend/src/luffy_agent/user/schemas.py', status: 'renamed' },
]
const tree = buildChangedTree(files)
const names = (dir: string): string[] => (tree.get(dir) ?? []).map(entry => entry.name)
it('目录层级照留,每层只有通往改过的文件的目录', () => {
expect(names('')).toEqual(['backend'])
expect(names('backend')).toEqual(['src', 'tests'])
expect(names('backend/src')).toEqual(['luffy_agent'])
expect(names('backend/src/luffy_agent')).toEqual(['core', 'user'])
expect(names('backend/src/luffy_agent/core')).toEqual(['storage.py'])
expect(names('backend/tests/user')).toEqual(['test_avatar.py'])
})
it('同一层里的文件按名字排,带着各自的改动和相对仓库根的路径', () => {
expect(tree.get('backend/src/luffy_agent/user')).toEqual([
{ name: 'legacy_profile.py', path: 'backend/src/luffy_agent/user/legacy_profile.py', isDirectory: false, change: 'D' },
{ name: 'routes.py', path: 'backend/src/luffy_agent/user/routes.py', isDirectory: false, change: 'M' },
{ name: 'schemas.py', path: 'backend/src/luffy_agent/user/schemas.py', isDirectory: false, change: 'M' },
{ name: 'services.py', path: 'backend/src/luffy_agent/user/services.py', isDirectory: false, change: 'M' },
])
})
it('目录没有改动,路径相对仓库根', () => {
expect(tree.get('backend')).toEqual([
{ name: 'src', path: 'backend/src', isDirectory: true },
{ name: 'tests', path: 'backend/tests', isDirectory: true },
])
})
it('目录在前、文件在后,名字不区分大小写,和普通的树一样', () => {
const mixed = buildChangedTree([
{ path: 'README.md', status: 'changed' },
{ path: 'src/a.ts', status: 'added' },
{ path: 'Docs/b.md', status: 'added' },
{ path: 'package.json', status: 'changed' },
])
expect(mixed.get('')?.map(entry => entry.name)).toEqual(['Docs', 'src', 'package.json', 'README.md'])
})
it('文件换成同名目录(删掉 a、新加 a/b)两项都留着', () => {
const replaced = buildChangedTree([{ path: 'a', status: 'deleted' }, { path: 'a/b', status: 'added' }])
expect(replaced.get('')).toEqual([
{ name: 'a', path: 'a', isDirectory: true },
{ name: 'a', path: 'a', isDirectory: false, change: 'D' },
])
expect(replaced.get('a')?.map(entry => entry.change)).toEqual(['A'])
})
it('没有改动的文件时根下面是空的', () => {
expect(buildChangedTree([]).get('')).toEqual([])
})
})