✨ 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:
1 parent
61c4728dce
commit
7f460d1257
18 files changed
+847
-240
No files matched your search
@@ -0,0 +1,191 @@
|
||||
import type { ExtensionContext, TreeView } from 'vscode'
|
||||
import type { GitApi, GitRepository } from '../../git/gitApi'
|
||||
import type { FileNode, FileTreeProvider } from '../explorer/fileTree'
|
||||
import type { TreeFilter, TreeFilters } from '../explorer/treeFilters'
|
||||
import { Disposable, window, workspace } from 'vscode'
|
||||
import { loadGitApi } from '../../git/gitExtension'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { buildChangedTree } from '../explorer/changedTree'
|
||||
import { uncommittedRoots, workspaceOrder } from './uncommittedFiles'
|
||||
|
||||
/** workspaceState:开着「未提交的更改」,重开窗口接着显示。 */
|
||||
const UNCOMMITTED_KEY = 'superpowers.codeReader.uncommitted'
|
||||
/** 开着时视图标题后面淡色的字(TreeView.description)。 */
|
||||
const UNCOMMITTED_DESCRIPTION = '未提交的更改'
|
||||
/** git 状态连着变(保存、暂存、切分支)时合成一次重摆。 */
|
||||
const REBUILD_DELAY_MS = 300
|
||||
|
||||
/** 怎么进的:enter 点按钮(出错时说一声)、restore 重开窗口接着显示(出错只记日志)。 */
|
||||
type How = 'enter' | 'restore'
|
||||
|
||||
function errorText(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏只看未提交的更改:树里只剩工作区各 git 仓库里没提交的文件(暂存的、没暂存的、未跟踪的、合并冲突的),每个有改动的仓库一个根。
|
||||
* 文件就是工作区里的文件,颜色和 M / A / D 徽标由 VS Code 自带的 git 装饰加上。开着时盯着 git 状态自动重摆;「显示全部」回到工作区文件树。
|
||||
* 开着没开按工作区记住;和审查工单互斥,见 TreeFilters。
|
||||
*/
|
||||
export class UncommittedMode implements TreeFilter, Disposable {
|
||||
private readonly disposables: Disposable[]
|
||||
/** 开着时的 git API;没开时 undefined */
|
||||
private api: GitApi | undefined
|
||||
/** 开着时盯 git 状态的监听 */
|
||||
private watching: Disposable | undefined
|
||||
private timer: NodeJS.Timeout | undefined
|
||||
/** 树上现在摆的改动(序列化),没变就不重画 */
|
||||
private shown: string | undefined
|
||||
/** 每进一次、退一次都加一:等 git 扩展回来时已经退出或又进了一次的,结果丢掉。 */
|
||||
private generation = 0
|
||||
|
||||
/** viewId 是侧栏视图的 id:刷新时在这个视图上转进度。 */
|
||||
constructor(
|
||||
private readonly context: ExtensionContext,
|
||||
private readonly viewId: string,
|
||||
private readonly view: TreeView<FileNode>,
|
||||
private readonly files: FileTreeProvider,
|
||||
private readonly filters: TreeFilters,
|
||||
) {
|
||||
filters.add(this)
|
||||
this.disposables = [filters.whenShown(() => void this.restore())]
|
||||
}
|
||||
|
||||
/** 「未提交的更改」:别的筛选退出,树里换成未提交的文件,之后跟着 git 状态变。 */
|
||||
async enter(): Promise<void> {
|
||||
await this.open('enter')
|
||||
}
|
||||
|
||||
/** 「显示全部」或换成别的筛选:不再盯 git、不再记着,正在进的作废;开着的回到工作区文件树。 */
|
||||
async exit(): Promise<void> {
|
||||
this.generation++
|
||||
this.stopWatching()
|
||||
await Promise.all([
|
||||
this.filters.leave(this),
|
||||
this.context.workspaceState.update(UNCOMMITTED_KEY, undefined),
|
||||
])
|
||||
}
|
||||
|
||||
/** 开着时的「刷新」:让 git 扩展重读工作区各仓库的状态,再整棵重摆。 */
|
||||
async reload(): Promise<void> {
|
||||
const api = this.api
|
||||
if (!api)
|
||||
return
|
||||
const folders = workspace.workspaceFolders ?? []
|
||||
const repositories = api.repositories.filter(repository => workspaceOrder(repository.rootUri.path, folders) >= 0)
|
||||
await window.withProgress({ location: { viewId: this.viewId } }, () => Promise.all(repositories.map(repository => this.status(repository))))
|
||||
if (this.api !== api)
|
||||
return
|
||||
this.shown = undefined
|
||||
this.rebuild()
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stopWatching()
|
||||
for (const disposable of this.disposables)
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
private async restore(): Promise<void> {
|
||||
if (this.context.workspaceState.get<boolean>(UNCOMMITTED_KEY))
|
||||
await this.open('restore')
|
||||
}
|
||||
|
||||
private async open(how: How): Promise<void> {
|
||||
const generation = ++this.generation
|
||||
let api: GitApi | undefined
|
||||
try {
|
||||
api = await loadGitApi()
|
||||
}
|
||||
catch (err) {
|
||||
this.fail(how, '激活 vscode.git 扩展失败', errorText(err))
|
||||
return
|
||||
}
|
||||
if (generation !== this.generation)
|
||||
return
|
||||
if (!api) {
|
||||
this.fail(how, 'vscode.git 扩展没装或被禁用了')
|
||||
return
|
||||
}
|
||||
this.stopWatching()
|
||||
this.api = api
|
||||
this.watching = this.watch(api)
|
||||
const entering = this.filters.enter(this, 'uncommitted')
|
||||
this.view.description = UNCOMMITTED_DESCRIPTION
|
||||
this.rebuild()
|
||||
await Promise.all([entering, this.context.workspaceState.update(UNCOMMITTED_KEY, true)])
|
||||
}
|
||||
|
||||
private fail(how: How, message: string, details?: string): void {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `未提交的更改:${message}`, ...(details ? { details } : {}) })
|
||||
if (how === 'enter')
|
||||
void window.showErrorMessage(`没法显示未提交的更改:${message}`)
|
||||
}
|
||||
|
||||
private async status(repository: GitRepository): Promise<void> {
|
||||
try {
|
||||
await repository.status()
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: '未提交的更改:重读 git 状态失败', details: `${repository.rootUri.fsPath}\n${errorText(err)}` })
|
||||
}
|
||||
}
|
||||
|
||||
/** 盯着 git:各仓库的状态变了、开了 / 关了仓库、git 扩展找完仓库、工作区文件夹变了,都合成一次重摆。 */
|
||||
private watch(api: GitApi): Disposable {
|
||||
const repositories = new Map<GitRepository, Disposable>()
|
||||
const schedule = (): void => {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => this.rebuild(), REBUILD_DELAY_MS)
|
||||
}
|
||||
const add = (repository: GitRepository): void => {
|
||||
if (!repositories.has(repository))
|
||||
repositories.set(repository, repository.state.onDidChange(schedule))
|
||||
}
|
||||
api.repositories.forEach(add)
|
||||
return Disposable.from(
|
||||
api.onDidOpenRepository((repository) => {
|
||||
add(repository)
|
||||
schedule()
|
||||
}),
|
||||
api.onDidCloseRepository((repository) => {
|
||||
repositories.get(repository)?.dispose()
|
||||
repositories.delete(repository)
|
||||
schedule()
|
||||
}),
|
||||
api.onDidChangeState(schedule),
|
||||
workspace.onDidChangeWorkspaceFolders(schedule),
|
||||
{ dispose: () => repositories.forEach(listener => listener.dispose()) },
|
||||
)
|
||||
}
|
||||
|
||||
private stopWatching(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = undefined
|
||||
this.watching?.dispose()
|
||||
this.watching = undefined
|
||||
this.api = undefined
|
||||
this.shown = undefined
|
||||
}
|
||||
|
||||
/** 按 git 现在的状态重摆树;和树上摆着的一样就不动。没有改动时标题下面写一行说明。 */
|
||||
private rebuild(): void {
|
||||
const api = this.api
|
||||
if (!api)
|
||||
return
|
||||
const folders = workspace.workspaceFolders ?? []
|
||||
const roots = uncommittedRoots(api.repositories, folders)
|
||||
this.view.message = roots.length > 0
|
||||
? undefined
|
||||
: api.state === 'initialized' ? '没有未提交的更改' : '正在读取 git 状态…'
|
||||
const shown = JSON.stringify(roots.map(root => [root.repository.rootUri.toString(), root.name, root.files]))
|
||||
if (shown === this.shown)
|
||||
return
|
||||
this.shown = shown
|
||||
this.files.showChanges(roots.map(root => ({
|
||||
// 仓库根不是工作区文件夹时(嵌套的仓库、包着工作区文件夹的仓库)拿仓库根当根,名字用仓库根目录的名字
|
||||
folder: root.folder ?? { uri: root.repository.rootUri, name: root.name, index: folders.length },
|
||||
tree: buildChangedTree(root.files),
|
||||
})))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user