✨ feat(vscode): 「未提交的更改」分暂存区两组、单文件暂存、隐藏测试文件
- 照源代码管理视图分「暂存的更改」「更改」两组(空的不摆,分组行写文件数),部分暂存的文件两组都有 - 「更改」组的文件行上加「暂存」按钮,走 git 扩展的 repository.add - 标题栏「隐藏测试文件 / 显示测试文件」开关,按工作区记住,藏了几个写在标题后面;测试文件的判断集中在 isTestFile - 以上只在「未提交的更改」里有:普通文件树、审查模式的行和菜单不变
This commit is contained in:
1 parent
a3b04a443b
commit
87d4360704
14 files changed
+528
-129
No files matched your search
@@ -1,17 +1,25 @@
|
||||
import type { ExtensionContext, TreeView } from 'vscode'
|
||||
import type { ExtensionContext, TreeView, WorkspaceFolder } from 'vscode'
|
||||
import type { GitApi, GitRepository } from '../../git/gitApi'
|
||||
import type { FileNode, FileTreeProvider } from '../explorer/fileTree'
|
||||
import type { ChangedFile } from '../explorer/changedTree'
|
||||
import type { ChangedSection, FileNode, FileTreeProvider, TreeNode } from '../explorer/fileTree'
|
||||
import type { TreeFilter, TreeFilters } from '../explorer/treeFilters'
|
||||
import { Disposable, window, workspace } from 'vscode'
|
||||
import type { UncommittedRoot } from './uncommittedFiles'
|
||||
import * as path from 'node:path'
|
||||
import { commands, 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'
|
||||
import { uncommittedDescription, uncommittedMessage, uncommittedRoots, UNSTAGED_MARKER, withoutTestFiles, workspaceOrder } from './uncommittedFiles'
|
||||
|
||||
/** workspaceState:开着「未提交的更改」,重开窗口接着显示。 */
|
||||
const UNCOMMITTED_KEY = 'superpowers.codeReader.uncommitted'
|
||||
/** 开着时视图标题后面淡色的字(TreeView.description)。 */
|
||||
const UNCOMMITTED_DESCRIPTION = '未提交的更改'
|
||||
/** workspaceState:隐藏测试文件。 */
|
||||
const HIDE_TESTS_KEY = 'superpowers.codeReader.hideTests'
|
||||
/** 上下文键:隐藏着测试文件。标题栏的「隐藏测试文件 / 显示测试文件」按它换着显示。 */
|
||||
const TESTS_HIDDEN_CONTEXT = 'superpowers.codeReader.testsHidden'
|
||||
/** 两个分组的名字,照 VS Code 源代码管理视图的中文界面。 */
|
||||
const STAGED_LABEL = '暂存的更改'
|
||||
const CHANGES_LABEL = '更改'
|
||||
/** git 状态连着变(保存、暂存、切分支)时合成一次重摆。 */
|
||||
const REBUILD_DELAY_MS = 300
|
||||
|
||||
@@ -23,9 +31,10 @@ function errorText(err: unknown): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏只看未提交的更改:树里只剩工作区各 git 仓库里没提交的文件(暂存的、没暂存的、未跟踪的、合并冲突的),每个有改动的仓库一个根。
|
||||
* 文件就是工作区里的文件,颜色和 M / A / D 徽标由 VS Code 自带的 git 装饰加上。开着时盯着 git 状态自动重摆;「显示全部」回到工作区文件树。
|
||||
* 开着没开按工作区记住;和审查工单互斥,见 TreeFilters。
|
||||
* 侧栏只看未提交的更改:照源代码管理视图分「暂存的更改」「更改」两组(空的不摆),每组下面每个有改动的仓库一个根。
|
||||
* 文件就是工作区里的文件,颜色和 M / A / D 徽标由 VS Code 自带的 git 装饰加上;「更改」组里的文件行上有「暂存」按钮。
|
||||
* 可以隐藏测试文件。开着时盯着 git 状态自动重摆;「显示全部」回到工作区文件树。
|
||||
* 开着没开、隐不隐藏测试文件都按工作区记住;和审查工单互斥,见 TreeFilters。
|
||||
*/
|
||||
export class UncommittedMode implements TreeFilter, Disposable {
|
||||
private readonly disposables: Disposable[]
|
||||
@@ -38,15 +47,18 @@ export class UncommittedMode implements TreeFilter, Disposable {
|
||||
private shown: string | undefined
|
||||
/** 每进一次、退一次都加一:等 git 扩展回来时已经退出或又进了一次的,结果丢掉。 */
|
||||
private generation = 0
|
||||
private hideTests: boolean
|
||||
|
||||
/** viewId 是侧栏视图的 id:刷新时在这个视图上转进度。 */
|
||||
constructor(
|
||||
private readonly context: ExtensionContext,
|
||||
private readonly viewId: string,
|
||||
private readonly view: TreeView<FileNode>,
|
||||
private readonly view: TreeView<TreeNode>,
|
||||
private readonly files: FileTreeProvider,
|
||||
private readonly filters: TreeFilters,
|
||||
) {
|
||||
this.hideTests = context.workspaceState.get<boolean>(HIDE_TESTS_KEY) ?? false
|
||||
void commands.executeCommand('setContext', TESTS_HIDDEN_CONTEXT, this.hideTests)
|
||||
filters.add(this)
|
||||
this.disposables = [filters.whenShown(() => void this.restore())]
|
||||
}
|
||||
@@ -80,6 +92,37 @@ export class UncommittedMode implements TreeFilter, Disposable {
|
||||
this.rebuild()
|
||||
}
|
||||
|
||||
/** 「隐藏测试文件 / 显示测试文件」:两组都剔除 / 放回测试文件,按工作区记住。 */
|
||||
async setTestsHidden(hidden: boolean): Promise<void> {
|
||||
this.hideTests = hidden
|
||||
this.rebuild()
|
||||
await Promise.all([
|
||||
commands.executeCommand('setContext', TESTS_HIDDEN_CONTEXT, hidden),
|
||||
this.context.workspaceState.update(HIDE_TESTS_KEY, hidden || undefined),
|
||||
])
|
||||
}
|
||||
|
||||
/** 「更改」组里文件行上的「暂存」:把这个文件放进暂存区(新文件、删掉的、冲突的都行),树跟着 git 状态变。 */
|
||||
async stage(node: FileNode | undefined): Promise<void> {
|
||||
const api = this.api
|
||||
if (!node || !api)
|
||||
return // 只挂在开着时「更改」组的文件行上
|
||||
const name = path.basename(node.uri.fsPath)
|
||||
const repository = api.getRepository(node.uri)
|
||||
if (!repository) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `未提交的更改:${name} 不在打开的 git 仓库里,没法暂存`, details: node.uri.fsPath })
|
||||
void window.showErrorMessage(`没法暂存 ${name}:找不到它所在的 git 仓库`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await repository.add([node.uri.fsPath])
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'error', source: 'codeReader', message: `未提交的更改:暂存 ${name} 失败`, details: `${node.uri.fsPath}\n${errorText(err)}` })
|
||||
void window.showErrorMessage(`没能暂存 ${name}:${errorText(err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stopWatching()
|
||||
for (const disposable of this.disposables)
|
||||
@@ -111,7 +154,6 @@ export class UncommittedMode implements TreeFilter, Disposable {
|
||||
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)])
|
||||
}
|
||||
@@ -168,24 +210,42 @@ export class UncommittedMode implements TreeFilter, Disposable {
|
||||
this.shown = undefined
|
||||
}
|
||||
|
||||
/** 按 git 现在的状态重摆树;和树上摆着的一样就不动。没有改动时标题下面写一行说明。 */
|
||||
/** 按 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]))
|
||||
const all = uncommittedRoots(api.repositories, folders)
|
||||
const { roots, hidden } = this.hideTests ? withoutTestFiles(all) : { roots: all, hidden: 0 }
|
||||
this.view.description = uncommittedDescription(hidden)
|
||||
this.view.message = uncommittedMessage({ hasFiles: roots.length > 0, hiddenTests: hidden, loading: api.state !== 'initialized' })
|
||||
const shown = JSON.stringify(roots.map(root => [root.repository.rootUri.toString(), root.name, root.staged, root.changes]))
|
||||
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),
|
||||
})))
|
||||
// 仓库根不是工作区文件夹时(嵌套的仓库、包着工作区文件夹的仓库)拿仓库根当根,名字用仓库根目录的名字
|
||||
const placed = roots.map(root => ({ root, folder: root.folder ?? { uri: root.repository.rootUri, name: root.name, index: folders.length } }))
|
||||
this.files.showChanges([
|
||||
section(STAGED_LABEL, placed, root => root.staged),
|
||||
section(CHANGES_LABEL, placed, root => root.changes, UNSTAGED_MARKER),
|
||||
].filter(group => group.roots.length > 0))
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个分组:有这个区的文件的仓库各一个根,分组行后面写这个区一共几个文件。 */
|
||||
function section(
|
||||
label: string,
|
||||
placed: readonly { root: UncommittedRoot<GitRepository, WorkspaceFolder>, folder: WorkspaceFolder }[],
|
||||
filesOf: (root: UncommittedRoot<GitRepository, WorkspaceFolder>) => ChangedFile[],
|
||||
fileMarker?: string,
|
||||
): ChangedSection {
|
||||
const withFiles = placed.filter(({ root }) => filesOf(root).length > 0)
|
||||
const count = withFiles.reduce((sum, { root }) => sum + filesOf(root).length, 0)
|
||||
return {
|
||||
label,
|
||||
description: String(count),
|
||||
...(fileMarker ? { fileMarker } : {}),
|
||||
roots: withFiles.map(({ root, folder }) => ({ folder, tree: buildChangedTree(filesOf(root)) })),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user