✨ feat(vscode): 代码阅读侧栏可以按审查工单只看 PR 改过的文件:标题栏「选工单」列出看板「审查」列,选中后树里只剩改过的文件(M / A / D 着色),点开的是本机工作副本里 PR 最新的代码,没有就拉 PR 分支建一个,「显示全部」退出
This commit is contained in:
1 parent
c009187f6c
commit
8e2cdfaf17
18 files changed
+1053
-40
No files matched your search
@@ -1,7 +1,9 @@
|
||||
import type { Event, TreeDataProvider, WorkspaceFolder } from 'vscode'
|
||||
import type { ChangedTree, FileChange } from '../review/changedFiles'
|
||||
import type { ExcludeSetting } from './fileEntries'
|
||||
import * as path from 'node:path'
|
||||
import { EventEmitter, FileType, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, workspace } from 'vscode'
|
||||
import { CHANGE_STYLES } from '../review/changedFiles'
|
||||
import { createExcludeMatcher, entryContext, sortEntries } from './fileEntries'
|
||||
|
||||
export interface FileNode {
|
||||
@@ -9,6 +11,14 @@ export interface FileNode {
|
||||
folder: WorkspaceFolder
|
||||
isDirectory: boolean
|
||||
isRoot: boolean
|
||||
/** 审查工单时这个文件在 PR 里怎么改的;普通的树、目录没有 */
|
||||
change?: FileChange
|
||||
}
|
||||
|
||||
/** 审查工单时树上摆什么:工作副本当唯一的根(folder),下面只有 PR 改过的文件(tree)。 */
|
||||
export interface ChangedFilesView {
|
||||
folder: WorkspaceFolder
|
||||
tree: ChangedTree
|
||||
}
|
||||
|
||||
/** 给树上的行添信息(比如文件注释):每次画行时调 decorate,onDidChange 报出哪些 Uri 的行要重画。 */
|
||||
@@ -23,6 +33,7 @@ const REFRESH_DELAY_MS = 300
|
||||
/**
|
||||
* 侧栏的工作区文件树:每个工作区文件夹一个根节点,只看和打开。
|
||||
* 节点用 resourceUri,图标、git 改动颜色都由 VS Code 按文件图标主题和 SCM 装饰自动加上。
|
||||
* 审查工单时换成只摆 PR 改过的文件(showChanges):目录全部展开,删掉的文件列出来但打不开。
|
||||
*/
|
||||
export class FileTreeProvider implements TreeDataProvider<FileNode> {
|
||||
/** undefined 是整棵树重画,数组是只重画这几行 */
|
||||
@@ -32,6 +43,8 @@ export class FileTreeProvider implements TreeDataProvider<FileNode> {
|
||||
/** 按 Uri 记下交给 VS Code 的节点:只重画几行时要交回同一个对象,VS Code 按对象认行。 */
|
||||
private readonly nodes = new Map<string, FileNode>()
|
||||
private timer: NodeJS.Timeout | undefined
|
||||
/** 审查工单时只摆的那些文件;undefined 是普通的工作区文件树 */
|
||||
private changes: ChangedFilesView | undefined
|
||||
|
||||
/**
|
||||
* @param openCommand 点文件时执行的命令,参数是文件 Uri
|
||||
@@ -68,7 +81,15 @@ export class FileTreeProvider implements TreeDataProvider<FileNode> {
|
||||
return item
|
||||
}
|
||||
|
||||
/** 换成只摆 PR 改过的文件(undefined 回到工作区文件树),整棵树重画。 */
|
||||
showChanges(view: ChangedFilesView | undefined): void {
|
||||
this.changes = view
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
async getChildren(node?: FileNode): Promise<FileNode[]> {
|
||||
if (this.changes)
|
||||
return this.changedChildren(this.changes, node)
|
||||
if (!node)
|
||||
return (workspace.workspaceFolders ?? []).map(folder => this.nodeAt(folder.uri, folder, true, true))
|
||||
|
||||
@@ -97,26 +118,38 @@ export class FileTreeProvider implements TreeDataProvider<FileNode> {
|
||||
if (!node.isDirectory) {
|
||||
const item = new TreeItem(node.uri, TreeItemCollapsibleState.None)
|
||||
item.iconPath = ThemeIcon.File
|
||||
item.command = { command: this.openCommand, title: '打开', arguments: [node.uri] }
|
||||
if (node.change === 'D')
|
||||
item.tooltip = CHANGE_STYLES.D.tooltip // 删掉的文件没有最新的版本可看,不挂打开的命令
|
||||
else
|
||||
item.command = { command: this.openCommand, title: '打开', arguments: [node.uri] }
|
||||
return item
|
||||
}
|
||||
const item = new TreeItem(node.uri, node.isRoot ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed)
|
||||
const expanded = node.isRoot || this.changes !== undefined
|
||||
const item = new TreeItem(node.uri, expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed)
|
||||
item.iconPath = ThemeIcon.Folder
|
||||
if (node.isRoot)
|
||||
item.label = node.folder.name
|
||||
return item
|
||||
}
|
||||
|
||||
private nodeAt(uri: Uri, folder: WorkspaceFolder, isDirectory: boolean, isRoot: boolean): FileNode {
|
||||
private nodeAt(uri: Uri, folder: WorkspaceFolder, isDirectory: boolean, isRoot: boolean, change?: FileChange): FileNode {
|
||||
const id = uri.toString()
|
||||
const known = this.nodes.get(id)
|
||||
if (known && known.folder === folder && known.isDirectory === isDirectory && known.isRoot === isRoot)
|
||||
if (known && known.folder === folder && known.isDirectory === isDirectory && known.isRoot === isRoot && known.change === change)
|
||||
return known
|
||||
const node: FileNode = { uri, folder, isDirectory, isRoot }
|
||||
const node: FileNode = { uri, folder, isDirectory, isRoot, ...(change ? { change } : {}) }
|
||||
this.nodes.set(id, node)
|
||||
return node
|
||||
}
|
||||
|
||||
/** 审查工单时的一层:根下面是工作副本,再往下只有 PR 改过的文件和通往它们的目录。 */
|
||||
private changedChildren({ folder, tree }: ChangedFilesView, node?: FileNode): FileNode[] {
|
||||
if (!node)
|
||||
return [this.nodeAt(folder.uri, folder, true, true)]
|
||||
const dir = path.posix.relative(folder.uri.path, node.uri.path)
|
||||
return (tree.get(dir) ?? []).map(entry => this.nodeAt(Uri.joinPath(folder.uri, entry.path), folder, entry.isDirectory, false, entry.change))
|
||||
}
|
||||
|
||||
/** 还没展开过的行不用管,展开时自然按最新的画。 */
|
||||
private redraw(uris: readonly Uri[]): void {
|
||||
const nodes = uris.flatMap(uri => this.nodes.get(uri.toString()) ?? [])
|
||||
|
||||
@@ -102,3 +102,36 @@ describe('右键菜单的注释', () => {
|
||||
expect(itemsFor('view/item/context', 'notes', root)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('审查工单', () => {
|
||||
const REVIEWING = 'superpowers.codeReader.reviewing'
|
||||
const PICK = 'superpowers.codeReader.pickReviewIssue'
|
||||
const SHOW_ALL = 'superpowers.codeReader.showAllFiles'
|
||||
|
||||
it('标题栏:第一个「选工单」,审查时才有「显示全部」,然后「刷新」', () => {
|
||||
const title = menus['view/title']
|
||||
.filter(item => item.when.startsWith('view == superpowers.codeReader.files'))
|
||||
.sort((a, b) => orderOf(a) - orderOf(b))
|
||||
expect(title.map(item => item.command)).toEqual([PICK, SHOW_ALL, 'superpowers.codeReader.refresh'])
|
||||
expect(title.every(item => item.group?.startsWith('navigation'))).toBe(true)
|
||||
expect(title.find(item => item.command === SHOW_ALL)?.when).toContain(`&& ${REVIEWING}`)
|
||||
expect(title.find(item => item.command === PICK)?.when).not.toContain(REVIEWING)
|
||||
})
|
||||
|
||||
it('按钮的字和图标:选工单 git-pull-request,显示全部 close', () => {
|
||||
expect(commands.find(item => item.command === PICK)).toMatchObject({ title: '选工单', icon: '$(git-pull-request)' })
|
||||
expect(commands.find(item => item.command === SHOW_ALL)).toMatchObject({ title: '显示全部', icon: '$(close)' })
|
||||
})
|
||||
|
||||
it('审查时行上没有注释按钮(工作副本里不记注释),复制照旧', () => {
|
||||
const noteItems = menus['view/item/context'].filter(item => item.command === ADD_NOTE || item.command === EDIT_NOTE)
|
||||
expect(noteItems.length).toBeGreaterThan(0)
|
||||
expect(noteItems.every(item => item.when.includes(`!${REVIEWING}`))).toBe(true)
|
||||
const copyItems = menus['view/item/context'].filter(item => item.submenu === COPY_MENU || item.command?.startsWith('superpowers.codeReader.copy'))
|
||||
expect(copyItems.some(item => item.when.includes(REVIEWING))).toBe(false)
|
||||
})
|
||||
|
||||
it('命令面板里「显示全部」只在审查时有', () => {
|
||||
expect(menus.commandPalette.find(item => item.command === SHOW_ALL)?.when).toBe(REVIEWING)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user