✨ feat(vscode): 代码阅读「未提交的更改」点文件行改成打开标准预览(和主栏一样不标差异),文件行上加「打开更改」按钮才打开对比上一次提交的预览,语言没有代码阅读的走 VS Code 自带的差异视图,删掉的文件行不挂这个按钮

This commit is contained in:
cruldra committed 2026-10-14 13:47:59 +11:00
1 parent 4b6e110a44
commit c2a13e5691
18 files changed
+211 -79

No files matched your search

@@ -34,6 +34,13 @@ describe('行的 contextValue', () => {
expect(entryContext({ isDirectory: true, isRoot: false }, 'unstaged')).toBe('folder')
expect(entryContext({ isDirectory: true, isRoot: true }, 'unstaged')).toBe('root')
})
it('删掉的文件行最后带 deleted(菜单按它不挂「打开更改」),改了、新加的不带', () => {
expect(entryContext({ isDirectory: false, isRoot: false, change: 'D' })).toBe('file deleted')
expect(entryContext({ isDirectory: false, isRoot: false, change: 'D' }, 'unstaged')).toBe('file unstaged deleted')
expect(entryContext({ isDirectory: false, isRoot: false, change: 'M' }, 'unstaged')).toBe('file unstaged')
expect(entryContext({ isDirectory: false, isRoot: false, change: 'A' })).toBe('file')
})
})
describe('files.exclude', () => {
@@ -1,3 +1,4 @@
import type { FileChange } from './changedTree'
import { Minimatch } from 'minimatch'
export interface DirEntry { name: string, isDirectory: boolean }
@@ -15,16 +16,20 @@ export function sortEntries<T extends DirEntry>(entries: T[]): T[] {
/** 分组行(「暂存的更改」「更改」)的 contextValue:不挂任何行菜单。 */
export const GROUP_CONTEXT = 'group'
/** 删掉的文件那一行的 contextValue 最后带的标记:菜单按它不挂打开它的按钮(它没有内容可打开)。 */
const DELETED_MARKER = 'deleted'
/**
* 树上一行的 contextValue:菜单的 when 子句按它分工作区根、目录、文件。
* 文件行可以带一个标记(「更改」区里能暂存的是 `file unstaged`),菜单按它给这些行多挂按钮;目录、根不带。
* 文件行可以带一个标记(「更改」区里能暂存的是 `file unstaged`),菜单按它给这些行多挂按钮;删掉的文件行最后再带上 `deleted`(`file unstaged deleted`);目录、根都不带。
*/
export function entryContext(entry: { isDirectory: boolean, isRoot: boolean }, fileMarker?: string): string {
export function entryContext(entry: { isDirectory: boolean, isRoot: boolean, change?: FileChange }, fileMarker?: string): string {
if (entry.isRoot)
return 'root'
if (entry.isDirectory)
return 'folder'
return fileMarker ? `file ${fileMarker}` : 'file'
const file = fileMarker ? `file ${fileMarker}` : 'file'
return entry.change === 'D' ? `${file} ${DELETED_MARKER}` : file
}
/** `files.exclude` 的值:true 排除;false 不排除;`{ when }` 只在有对应的兄弟文件时排除。 */
@@ -249,7 +249,7 @@ describe('收藏栏:一个目录下的完整文件树', () => {
})
describe('点文件打开', () => {
it('命令带上文件和这一栏的 from:「未提交的更改」那一栏的是 uncommitted;删掉的文件不挂命令', async () => {
it('命令带上文件和这一栏给的 from;删掉的文件不挂命令,行的 contextValue 带 deleted(菜单按它不挂「打开更改」)', async () => {
const files = new FileTreeProvider({ command: 'open', from: 'uncommitted' }, decorator)
files.show({ kind: 'changes', view: [{ label: '更改', roots: [{ folder, tree: buildChangedTree([
{ path: 'backend/src/gate.py', change: 'M' },
@@ -260,6 +260,8 @@ describe('点文件打开', () => {
expect(files.getTreeItem(gate!).command).toEqual({ command: 'open', title: '打开', arguments: [gate!.uri, 'uncommitted'] })
expect(files.getTreeItem(readme!).command).toBeUndefined()
expect(files.getTreeItem(gate!).contextValue).toBe('file')
expect(files.getTreeItem(readme!).contextValue).toBe('file deleted')
})
it('别的栏的是 other', async () => {
+40 -13
View File
@@ -106,6 +106,7 @@ const COPY_MENU = 'superpowers.codeReader.copy'
const ADD_NOTE = 'superpowers.codeReader.addNote'
const EDIT_NOTE = 'superpowers.codeReader.editNote'
const STAGE = 'superpowers.codeReader.stage'
const OPEN_CHANGES = 'superpowers.codeReader.openChanges'
const ASK = 'superpowers.codeReader.askPath'
const ADD_FAVORITE = 'superpowers.codeReader.addFavorite'
@@ -114,6 +115,9 @@ const folder = entryContext({ isDirectory: true, isRoot: false })
const root = entryContext({ isDirectory: true, isRoot: true })
/** 「未提交的更改」里「更改」组的文件行 */
const unstaged = entryContext({ isDirectory: false, isRoot: false }, UNSTAGED_MARKER)
/** 删掉的文件行:「暂存的更改」组的、「更改」组的 */
const deleted = entryContext({ isDirectory: false, isRoot: false, change: 'D' })
const unstagedDeleted = entryContext({ isDirectory: false, isRoot: false, change: 'D' }, UNSTAGED_MARKER)
const noted = (context: string): string => rowDecoration(context, 'x', '注释', undefined)!.contextValue
const orderOf = (item: MenuItem): number => Number(item.group?.split('@')[1] ?? 0)
@@ -161,10 +165,11 @@ describe('菜单的 when 子句', () => {
})
describe('侧栏行尾按钮', () => {
it('每一栏都是先「问 AI」再「复制」再注释:没注释的是「添加注释」,有注释的是「编辑注释」;根都没有', () => {
it('每一栏都是先「问 AI」再「复制」再注释:没注释的是「添加注释」,有注释的是「编辑注释」(「未提交的更改」的文件行后面还有「打开更改」);根都没有', () => {
for (const view of READER_VIEWS) {
expect(itemsFor('view/item/context', 'inline', file, view)).toEqual([ASK, COPY_MENU, ADD_NOTE])
expect(itemsFor('view/item/context', 'inline', noted(file), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE])
const openChanges = view === UNCOMMITTED ? [OPEN_CHANGES] : []
expect(itemsFor('view/item/context', 'inline', file, view)).toEqual([ASK, COPY_MENU, ADD_NOTE, ...openChanges])
expect(itemsFor('view/item/context', 'inline', noted(file), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE, ...openChanges])
expect(itemsFor('view/item/context', 'inline', root, view)).toEqual([])
}
})
@@ -177,15 +182,36 @@ describe('侧栏行尾按钮', () => {
expect(itemsFor('view/item/context', 'inline', folder, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE])
})
it('「未提交的更改」里「更改」组的文件行多一个「暂存」,排在最后;别的行、别的栏都没有', () => {
expect(itemsFor('view/item/context', 'inline', unstaged, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE, STAGE])
expect(itemsFor('view/item/context', 'inline', noted(unstaged), UNCOMMITTED)).toEqual([ASK, COPY_MENU, EDIT_NOTE, STAGE])
for (const context of [file, folder, root, noted(file), noted(folder)])
it('「未提交的更改」里「更改」组的文件行多一个「暂存」,排在最后(删掉的文件也有);别的行、别的栏都没有', () => {
expect(itemsFor('view/item/context', 'inline', unstaged, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE, OPEN_CHANGES, STAGE])
expect(itemsFor('view/item/context', 'inline', noted(unstaged), UNCOMMITTED)).toEqual([ASK, COPY_MENU, EDIT_NOTE, OPEN_CHANGES, STAGE])
expect(itemsFor('view/item/context', 'inline', unstagedDeleted, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE, STAGE])
expect(itemsFor('view/item/context', 'inline', noted(unstagedDeleted), UNCOMMITTED)).toEqual([ASK, COPY_MENU, EDIT_NOTE, STAGE])
for (const context of [file, folder, root, deleted, noted(file), noted(folder)])
expect(itemsFor('view/item/context', 'inline', context, UNCOMMITTED)).not.toContain(STAGE)
for (const view of [FILES, FAVORITE])
expect(itemsFor('view/item/context', 'inline', unstaged, view)).not.toContain(STAGE)
})
it('「未提交的更改」里两组的文件行都有「打开更改」,排在「暂存」前面;删掉的文件、目录、根、别的栏都没有', () => {
for (const context of [file, unstaged, noted(file), noted(unstaged)])
expect(itemsFor('view/item/context', 'inline', context, UNCOMMITTED)).toContain(OPEN_CHANGES)
for (const context of [deleted, unstagedDeleted, noted(deleted), noted(unstagedDeleted), folder, root, GROUP_CONTEXT])
expect(itemsFor('view/item/context', 'inline', context, UNCOMMITTED)).not.toContain(OPEN_CHANGES)
for (const view of [FILES, FAVORITE, FEATURE]) {
for (const context of [file, unstaged])
expect(itemsFor('view/item/context', 'inline', context, view)).not.toContain(OPEN_CHANGES)
}
})
it('删掉的文件行上别的按钮照常(问 AI、复制、注释),审查时主栏里的也一样', () => {
for (const view of READER_VIEWS) {
expect(itemsFor('view/item/context', 'inline', deleted, view)).toEqual([ASK, COPY_MENU, ADD_NOTE])
expect(itemsFor('view/item/context', 'inline', noted(deleted), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE])
}
expect(itemsFor('view/item/context', 'inline', deleted, FILES, { [REVIEWING]: true })).toEqual([ASK, COPY_MENU])
})
it('分组行(「暂存的更改」「更改」)什么按钮、菜单都没有', () => {
for (const group of ['inline', 'copy', 'fileManager', 'notes', 'star'])
expect(itemsFor('view/item/context', group, GROUP_CONTEXT, UNCOMMITTED)).toEqual([])
@@ -199,7 +225,7 @@ describe('侧栏行尾按钮', () => {
})
it('「问 AI」文件和文件夹(含未提交、有注释)都有,审查时也不藏,命令面板里没有', () => {
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
for (const context of [file, folder, unstaged, deleted, unstagedDeleted, noted(file), noted(folder), noted(unstaged), noted(deleted)])
expect(itemsFor('view/item/context', 'inline', context, FILES, { [REVIEWING]: true })[0]).toBe(ASK)
expect(titleOf(ASK)).toMatchObject({ title: '问 AI', icon: '$(sparkle)' })
expect(palette.get(ASK)).toBe('false')
@@ -209,7 +235,7 @@ describe('侧栏行尾按钮', () => {
describe('复制菜单', () => {
it.each([COPY_MENU, 'view/item/context'])('%s:每一栏都是文件四项、文件夹没有「不带后缀」,有没有注释都一样', (menu) => {
for (const view of READER_VIEWS) {
for (const context of [file, noted(file), unstaged, noted(unstaged)])
for (const context of [file, noted(file), unstaged, noted(unstaged), deleted, unstagedDeleted, noted(unstagedDeleted)])
expect(itemsFor(menu, 'copy', context, view)).toEqual(FILE_COPIES)
for (const context of [folder, noted(folder)])
expect(itemsFor(menu, 'copy', context, view)).toEqual(FOLDER_COPIES)
@@ -235,7 +261,7 @@ describe('复制菜单', () => {
describe('右键菜单的注释', () => {
it('每一栏没注释的目录 / 文件只有「添加注释」,有注释的只有「编辑注释」,根没有', () => {
for (const view of READER_VIEWS) {
for (const context of [file, folder, unstaged]) {
for (const context of [file, folder, unstaged, deleted, unstagedDeleted]) {
expect(itemsFor('view/item/context', 'notes', context, view)).toEqual([ADD_NOTE])
expect(itemsFor('view/item/context', 'notes', noted(context), view)).toEqual([EDIT_NOTE])
}
@@ -256,7 +282,7 @@ describe('右键菜单的「在系统文件管理器中显示」', () => {
for (const { platform, command } of REVEALS) {
const system = { isMac: false, [platform]: true, [REVIEWING]: true }
for (const view of READER_VIEWS) {
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
for (const context of [file, folder, unstaged, deleted, unstagedDeleted, noted(file), noted(folder), noted(unstaged), noted(unstagedDeleted)])
expect(itemsFor('view/item/context', 'fileManager', context, view, system)).toEqual([command])
expect(itemsFor('view/item/context', 'fileManager', root, view, system)).toEqual([])
}
@@ -303,7 +329,7 @@ describe('主栏的审查工单', () => {
expect(itemsFor('view/item/context', group, folder, FILES, reviewing)).toEqual([])
expect(itemsFor('view/item/context', 'copy', file, FILES, reviewing)).toEqual(FILE_COPIES)
expect(itemsFor('view/item/context', 'inline', folder, FAVORITE, reviewing)).toEqual([ASK, COPY_MENU, ADD_NOTE, ADD_FAVORITE])
expect(itemsFor('view/item/context', 'inline', file, UNCOMMITTED, reviewing)).toEqual([ASK, COPY_MENU, ADD_NOTE])
expect(itemsFor('view/item/context', 'inline', file, UNCOMMITTED, reviewing)).toEqual([ASK, COPY_MENU, ADD_NOTE, OPEN_CHANGES])
})
it('命令面板:「显示全部」、隐藏 / 显示测试文件只在审查时有', () => {
@@ -346,7 +372,8 @@ describe('「未提交的更改」一栏', () => {
expect(titleOf(SHOW_TESTS)).toMatchObject({ title: '显示测试文件', icon: '$(beaker)' })
expect(titleOf(REFRESH)).toMatchObject({ title: '刷新', icon: '$(refresh)' })
expect(titleOf(STAGE)).toMatchObject({ title: '暂存', icon: '$(add)' })
for (const id of [HIDE_TESTS, SHOW_TESTS, REFRESH, STAGE])
expect(titleOf(OPEN_CHANGES)).toMatchObject({ title: '打开更改', icon: '$(git-compare)' })
for (const id of [HIDE_TESTS, SHOW_TESTS, REFRESH, STAGE, OPEN_CHANGES])
expect(palette.get(id)).toBe('false')
})
})