✨ 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

+20 -31
View File
@@ -3,12 +3,14 @@ import type { PrFileDto } from '../../gitea/api'
import type { RepoContext } from '../../gitea/repoContext'
import type { Issue } from '../../gitea/types'
import type { FileNode, FileTreeProvider } from '../explorer/fileTree'
import type { TreeFilter, TreeFilters } from '../explorer/treeFilters'
import type { FileCount } from './pickList'
import * as path from 'node:path'
import { commands, QuickPickItemKind, Uri, window, workspace } from 'vscode'
import { QuickPickItemKind, Uri, window, workspace } from 'vscode'
import { logger } from '../../logging/logger'
import { buildChangedTree } from '../explorer/changedTree'
import { ChangeDecorations } from './changeDecorations'
import { buildChangedTree, changeOf } from './changedFiles'
import { changeOf } from './changedFiles'
import { countingFiles, pickEntries, pickItemText, pickRows, withFileCount } from './pickList'
import { columnLabel, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewDescription, reviewMessage } from './reviewRules'
import { loadIssue, loadPullRequest, loadPullRequestFiles, requireRepo, reviewCandidateIssues } from './reviewSource'
@@ -16,8 +18,6 @@ import { localWorktree, prepareWorktree } from './reviewWorktree'
/** workspaceState:正在审查的工单号,重开窗口接着审查。 */
const REVIEW_ISSUE_KEY = 'superpowers.codeReader.reviewIssue'
/** 上下文键:正在审查工单。标题栏的「显示全部」、行上的注释按钮按它显示 / 藏起来。 */
const REVIEWING_CONTEXT = 'superpowers.codeReader.reviewing'
/**
* 怎么进的审查,决定不能审查、出错时怎么收场:
@@ -45,8 +45,9 @@ function errorText(err: unknown): string {
/**
* 侧栏按审查工单筛文件:「选工单」列出看板「审查」「进行中」列里能审查的工单,选中后树里只剩这张工单的 PR 改过的文件(按改动着色),
* 点开的是本机这张工单的工作副本里 PR 最新的代码;「显示全部」回到工作区文件树。选的工单按工作区记住。
* 和别的筛选(未提交的更改)互斥,见 TreeFilters。
*/
export class ReviewMode implements Disposable {
export class ReviewMode implements TreeFilter, Disposable {
private readonly decorations = new ChangeDecorations()
private readonly disposables: Disposable[]
/** 正在审查的工单号 */
@@ -60,25 +61,14 @@ export class ReviewMode implements Disposable {
private readonly viewId: string,
private readonly view: TreeView<FileNode>,
private readonly files: FileTreeProvider,
private readonly filters: TreeFilters,
) {
this.disposables = [this.decorations, window.registerFileDecorationProvider(this.decorations)]
// 视图第一次露出来时才接着上次的审查:没打开侧栏就不去拉 PR、动工作副本
if (view.visible) {
void this.restore()
}
else {
const shown = view.onDidChangeVisibility(({ visible }) => {
if (!visible)
return
shown.dispose()
void this.restore()
})
this.disposables.push(shown)
}
}
get active(): boolean {
return this.reviewing !== undefined
filters.add(this)
this.disposables = [
this.decorations,
window.registerFileDecorationProvider(this.decorations),
filters.whenShown(() => void this.restore()),
]
}
/**
@@ -156,16 +146,13 @@ export class ReviewMode implements Disposable {
: []))
}
/** 「显示全部」:回到工作区文件树,不再记着这张工单。 */
/** 「显示全部」或换成别的筛选:不再审查、不再记着这张工单,正在进的审查作废;正审查着的回到工作区文件树。 */
async exit(): Promise<void> {
this.generation++
this.reviewing = undefined
this.files.showChanges(undefined)
this.decorations.show([])
this.view.description = undefined
this.view.message = undefined
await Promise.all([
commands.executeCommand('setContext', REVIEWING_CONTEXT, false),
this.filters.leave(this),
this.context.workspaceState.update(REVIEW_ISSUE_KEY, undefined),
])
}
@@ -261,13 +248,15 @@ export class ReviewMode implements Disposable {
const root = Uri.file(worktree)
// 工作副本当根,名字照写工作区文件夹的:看的还是这个仓库,只是 PR 最新的代码
const folder: WorkspaceFolder = { uri: root, name: workspace.workspaceFolders?.[0]?.name ?? path.basename(worktree), index: 0 }
const changes = files.map(file => ({ path: file.path, change: changeOf(file.status) }))
const entering = this.filters.enter(this, 'review')
this.reviewing = issue.number
this.files.showChanges({ folder, tree: buildChangedTree(files) })
this.decorations.show(files.map(file => ({ uri: Uri.joinPath(root, file.path), change: changeOf(file.status) })))
this.files.showChanges([{ folder, tree: buildChangedTree(changes) }])
this.decorations.show(changes.map(file => ({ uri: Uri.joinPath(root, file.path), change: file.change })))
this.view.description = reviewDescription(issue.number)
this.view.message = reviewMessage(issue, pr, files.length)
await Promise.all([
commands.executeCommand('setContext', REVIEWING_CONTEXT, true),
entering,
this.context.workspaceState.update(REVIEW_ISSUE_KEY, issue.number),
])
}