✨ feat(vscode): 代码阅读侧栏拆成多栏:常驻未提交的更改、收藏目录各占一栏

This commit is contained in:
cruldra committed 2026-10-12 14:29:48 +11:00
1 parent 45e526afa9
commit b40ddb5e3a
27 files changed
+2427 -674

No files matched your search

+271 -124
View File
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
import { describe, expect, it } from 'vitest'
import { FAVORITE_SLOTS, favoriteCommand, favoriteContext, favoriteViewId } from '../favorites/favoriteSlots'
import { UNSTAGED_MARKER } from '../uncommitted/uncommittedFiles'
import { COPY_COMMANDS } from './copyText'
import { entryContext, GROUP_CONTEXT } from './fileEntries'
@@ -12,38 +13,126 @@ interface Manifest {
commands: { command: string, title: string, icon?: string }[]
submenus: { id: string, label: string, icon?: string }[]
menus: Record<string, MenuItem[]>
views: Record<string, { id: string, name: string, when?: string }[]>
}
}
const { commands, submenus, menus } = (JSON.parse(readFileSync(path.resolve(__dirname, '../../../package.json'), 'utf8')) as Manifest).contributes
const { commands, submenus, menus, views } = (JSON.parse(readFileSync(path.resolve(__dirname, '../../../package.json'), 'utf8')) as Manifest).contributes
/** 上下文键的值:没设的是 undefined */
type Context = Record<string, string | boolean | string[] | undefined>
/**
* 照 VS Code 的规则算 when 子句,够菜单里用的写法:`&&` `||` `!` 括号、`==` `!=`、`=~ /正则/`、`in 列表`、单独一个键(有值算真)。
* `&&` 比 `||` 先算,和 VS Code 一样。
*/
function evaluate(when: string, context: Context): boolean {
let at = 0
const skip = (): void => {
while (when[at] === ' ')
at++
}
const eat = (token: string): boolean => {
skip()
if (!when.startsWith(token, at))
return false
at += token.length
return true
}
const read = (pattern: RegExp): RegExpExecArray => {
skip()
const match = pattern.exec(when.slice(at))
if (!match)
throw new Error(`读不懂 when:${when}(在「${when.slice(at)}」)`)
at += match[0].length
return match
}
const primary = (): boolean => {
if (eat('(')) {
const value = or()
if (!eat(')'))
throw new Error(`括号没配对:${when}`)
return value
}
const key = read(/^[\w.]+/)[0]
if (eat('=~')) {
const [, source, flags] = read(/^\/((?:\\.|[^/\\])*)\/([a-z]*)/)
return new RegExp(source, flags).test(String(context[key] ?? ''))
}
if (eat('=='))
return String(context[key]) === read(/^[\w.]+/)[0]
if (eat('!='))
return String(context[key]) !== read(/^[\w.]+/)[0]
if (eat('in ')) {
const list = context[read(/^[\w.]+/)[0]]
return Array.isArray(list) && list.includes(String(context[key]))
}
return Boolean(context[key])
}
const unary = (): boolean => eat('!') ? !unary() : primary()
function and(): boolean {
let value = unary()
while (eat('&&')) {
const right = unary()
value = value && right
}
return value
}
function or(): boolean {
let value = and()
while (eat('||')) {
const right = and()
value = value || right
}
return value
}
const value = or()
skip()
if (at !== when.length)
throw new Error(`when 没读完:${when}`)
return value
}
const FILES = 'superpowers.codeReader.files'
const UNCOMMITTED = 'superpowers.codeReader.uncommitted'
const FAVORITE = favoriteViewId(7)
const READER_VIEWS = [FILES, UNCOMMITTED, ...FAVORITE_SLOTS.map(favoriteViewId)]
const REVIEWING = 'superpowers.codeReader.reviewing'
const COPY_MENU = 'superpowers.codeReader.copy'
const ADD_NOTE = 'superpowers.codeReader.addNote'
const EDIT_NOTE = 'superpowers.codeReader.editNote'
const STAGE = 'superpowers.codeReader.stage'
const ASK = 'superpowers.codeReader.askPath'
const ADD_FAVORITE = 'superpowers.codeReader.addFavorite'
const file = entryContext({ isDirectory: false, isRoot: false })
const folder = entryContext({ isDirectory: true, isRoot: false })
const root = entryContext({ isDirectory: true, isRoot: true })
/** 「未提交的更改」里「更改」组的文件行 */
const unstaged = entryContext({ isDirectory: false, isRoot: false }, UNSTAGED_MARKER)
const STAGE = 'superpowers.codeReader.stage'
const ASK = 'superpowers.codeReader.askPath'
const noted = (context: string): string => rowDecoration(context, 'x', '注释', undefined)!.contextValue
const orderOf = (item: MenuItem): number => Number(item.group?.split('@')[1] ?? 0)
const palette = new Map(menus.commandPalette.map(item => [item.command, item.when]))
const titleOf = (id: string): { title: string, icon?: string } | undefined => commands.find(item => item.command === id)
/** 某个菜单的某一组(行尾按钮 inline / 右键菜单的 copy、notes)对这一行显示哪些命令或子菜单,照 `@序号` 排 */
function itemsFor(menu: string, group: string, contextValue: string): string[] {
/** 在 view 这一栏(context 是别的上下文键)的某一行上,某个菜单的某一组(行尾按钮 inline / 右键菜单的 copy、notes…)显示哪些命令或子菜单,照 `@序号` 排 */
function itemsFor(menu: string, group: string, row: string, view = FILES, context: Context = {}): string[] {
return menus[menu]
.filter(item => item.group?.split('@')[0] === group && item.when.includes('view == superpowers.codeReader.files'))
.filter((item) => {
const pattern = /viewItem =~ \/(.+)\/$/.exec(item.when)?.[1]
return pattern !== undefined && new RegExp(pattern).test(contextValue)
})
.filter(item => item.group?.split('@')[0] === group && evaluate(item.when, { isMac: true, ...context, view, viewItem: row }))
.sort((a, b) => orderOf(a) - orderOf(b))
.map(item => item.command ?? item.submenu ?? '')
}
/** view 这一栏标题栏上显示哪些按钮,照 `@序号` 排 */
function titleBar(view: string, context: Context = {}): string[] {
return menus['view/title']
.filter(item => evaluate(item.when, { ...context, view }))
.sort((a, b) => orderOf(a) - orderOf(b))
.map(item => item.command ?? '')
}
const FILE_COPIES = [
'superpowers.codeReader.copyFileName',
'superpowers.codeReader.copyFileStem',
@@ -56,70 +145,84 @@ const FOLDER_COPIES = [
'superpowers.codeReader.copyAbsolutePath',
]
describe('侧栏行尾按钮', () => {
it('先「问 AI」再「复制」再注释:没注释的是「添加注释」,有注释的是「编辑注释」;工作区根都没有', () => {
for (const context of [file, folder]) {
expect(itemsFor('view/item/context', 'inline', context)).toEqual([ASK, COPY_MENU, ADD_NOTE])
expect(itemsFor('view/item/context', 'inline', noted(context))).toEqual([ASK, COPY_MENU, EDIT_NOTE])
}
expect(itemsFor('view/item/context', 'inline', root)).toEqual([])
describe('菜单的 when 子句', () => {
it('每一条都读得懂(括号配对、正则写全)', () => {
for (const item of Object.values(menus).flat())
expect(() => evaluate(item.when, {})).not.toThrow()
})
it('「更改」组的文件行多一个「暂存」,排在最后;别的行都没有', () => {
expect(itemsFor('view/item/context', 'inline', unstaged)).toEqual([ASK, COPY_MENU, ADD_NOTE, STAGE])
expect(itemsFor('view/item/context', 'inline', noted(unstaged))).toEqual([ASK, COPY_MENU, EDIT_NOTE, STAGE])
it('行菜单只挂在代码阅读的各栏,别的视图(看板、资源管理器)上没有', () => {
for (const view of ['superpowers.kanban', 'workbench.explorer.fileView'])
expect(itemsFor('view/item/context', 'inline', file, view)).toEqual([])
})
})
describe('侧栏行尾按钮', () => {
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])
expect(itemsFor('view/item/context', 'inline', root, view)).toEqual([])
}
})
it('目录行在主栏、收藏栏多一个「收藏目录」,排在最后;「未提交的更改」里没有', () => {
for (const view of [FILES, FAVORITE]) {
expect(itemsFor('view/item/context', 'inline', folder, view)).toEqual([ASK, COPY_MENU, ADD_NOTE, ADD_FAVORITE])
expect(itemsFor('view/item/context', 'inline', noted(folder), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE, ADD_FAVORITE])
}
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)])
expect(itemsFor('view/item/context', 'inline', context)).not.toContain(STAGE)
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 group of ['inline', 'copy', 'fileManager', 'notes'])
expect(itemsFor('view/item/context', group, GROUP_CONTEXT)).toEqual([])
expect(itemsFor(COPY_MENU, 'copy', GROUP_CONTEXT)).toEqual([])
for (const group of ['inline', 'copy', 'fileManager', 'notes', 'star'])
expect(itemsFor('view/item/context', group, GROUP_CONTEXT, UNCOMMITTED)).toEqual([])
expect(itemsFor(COPY_MENU, 'copy', GROUP_CONTEXT, UNCOMMITTED)).toEqual([])
})
it('「复制」是带 copy 图标的子菜单(VS Code 画成点开的下拉按钮),注释按钮的图标是 note / edit', () => {
expect(submenus.find(item => item.id === COPY_MENU)).toEqual({ id: COPY_MENU, label: '复制', icon: '$(copy)' })
expect(commands.find(item => item.command === ADD_NOTE)?.icon).toBe('$(note)')
expect(commands.find(item => item.command === EDIT_NOTE)?.icon).toBe('$(edit)')
expect(titleOf(ADD_NOTE)?.icon).toBe('$(note)')
expect(titleOf(EDIT_NOTE)?.icon).toBe('$(edit)')
})
it('「问 AI」在复制左边,文件和文件夹(含未提交、有注释)都有,审查时也不藏,命令面板里没有', () => {
const ask = menus['view/item/context'].filter(item => item.command === ASK)
expect(ask).toEqual([{
command: ASK,
when: 'view == superpowers.codeReader.files && viewItem =~ /^(file( unstaged)?|folder)( noted)?$/',
group: 'inline@0',
}])
expect(ask[0].when.includes('filter')).toBe(false)
it('「问 AI」文件和文件夹(含未提交、有注释)都有,审查时也不藏,命令面板里没有', () => {
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
expect(itemsFor('view/item/context', 'inline', context)[0]).toBe(ASK)
expect(itemsFor('view/item/context', 'inline', root)).not.toContain(ASK)
expect(commands.find(item => item.command === ASK)).toMatchObject({ title: '问 AI', icon: '$(sparkle)' })
expect(menus.commandPalette.find(item => item.command === ASK)?.when).toBe('false')
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')
})
})
describe('复制菜单', () => {
it.each([COPY_MENU, 'view/item/context'])('%s:文件四项、文件夹没有「不带后缀」,有没有注释都一样', (menu) => {
for (const context of [file, noted(file), unstaged, noted(unstaged)])
expect(itemsFor(menu, 'copy', context)).toEqual(FILE_COPIES)
for (const context of [folder, noted(folder)])
expect(itemsFor(menu, 'copy', context)).toEqual(FOLDER_COPIES)
expect(itemsFor(menu, 'copy', root)).toEqual([])
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)])
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)
expect(itemsFor(menu, 'copy', root, view)).toEqual([])
}
})
it('菜单上的字', () => {
const title = (id: string): string | undefined => commands.find(item => item.command === id)?.title
expect(FILE_COPIES.map(title)).toEqual(['复制文件名', '复制文件名(不带后缀)', '复制相对路径', '复制绝对路径'])
expect(title('superpowers.codeReader.copyFolderName')).toBe('复制文件夹名')
expect(FILE_COPIES.map(id => titleOf(id)?.title)).toEqual(['复制文件名', '复制文件名(不带后缀)', '复制相对路径', '复制绝对路径'])
expect(titleOf('superpowers.codeReader.copyFolderName')?.title).toBe('复制文件夹名')
})
it('命令都登记了、都有注册,命令面板里藏起来', () => {
const palette = new Map(menus.commandPalette.map(item => [item.command, item.when]))
for (const id of [...new Set([...FILE_COPIES, ...FOLDER_COPIES])]) {
expect(id in COPY_COMMANDS).toBe(true)
expect(commands.some(item => item.command === id)).toBe(true)
expect(titleOf(id)).toBeDefined()
expect(palette.get(id)).toBe('false')
}
expect(Object.keys(COPY_COMMANDS).sort()).toEqual([...new Set([...FILE_COPIES, ...FOLDER_COPIES])].sort())
@@ -127,12 +230,14 @@ describe('复制菜单', () => {
})
describe('右键菜单的注释', () => {
it('没注释的目录 / 文件只有「添加注释」,有注释的只有「编辑注释」,工作区根没有', () => {
for (const context of [file, folder, unstaged]) {
expect(itemsFor('view/item/context', 'notes', context)).toEqual([ADD_NOTE])
expect(itemsFor('view/item/context', 'notes', noted(context))).toEqual([EDIT_NOTE])
it('每一栏没注释的目录 / 文件只有「添加注释」,有注释的只有「编辑注释」,根没有', () => {
for (const view of READER_VIEWS) {
for (const context of [file, folder, unstaged]) {
expect(itemsFor('view/item/context', 'notes', context, view)).toEqual([ADD_NOTE])
expect(itemsFor('view/item/context', 'notes', noted(context), view)).toEqual([EDIT_NOTE])
}
expect(itemsFor('view/item/context', 'notes', root, view)).toEqual([])
}
expect(itemsFor('view/item/context', 'notes', root)).toEqual([])
})
})
@@ -143,101 +248,143 @@ describe('右键菜单的「在系统文件管理器中显示」', () => {
{ platform: 'isWindows', command: 'superpowers.codeReader.revealInFileExplorer', title: '在文件资源管理器中显示' },
{ platform: 'isLinux', command: 'superpowers.codeReader.openContainingFolder', title: '打开所在的文件夹' },
]
const REVEAL_IDS = REVEALS.map(item => item.command)
const whenOf = (command: string): string | undefined => menus['view/item/context'].find(item => item.command === command)?.when
it('文件、文件夹(含未提交、有注释)都有,工作区根没有;审查时也不藏', () => {
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
expect(itemsFor('view/item/context', 'fileManager', context)).toEqual(REVEAL_IDS)
expect(itemsFor('view/item/context', 'fileManager', root)).toEqual([])
expect(REVEAL_IDS.some(id => whenOf(id)?.includes('filter'))).toBe(false)
})
it('每个系统只出自己那一项,字和 VS Code 自带的一样', () => {
for (const { platform, command, title } of REVEALS) {
const when = whenOf(command)
expect(when).toContain(`&& ${platform} &&`)
for (const other of REVEALS.filter(item => item.platform !== platform))
expect(when).not.toContain(other.platform)
expect(commands.find(item => item.command === command)?.title).toBe(title)
it('每个系统只出自己那一项:每一栏的文件、文件夹(含未提交、有注释)都有,根没有;审查时也不藏', () => {
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)])
expect(itemsFor('view/item/context', 'fileManager', context, view, system)).toEqual([command])
expect(itemsFor('view/item/context', 'fileManager', root, view, system)).toEqual([])
}
}
})
it('自成一组,排在复制后面、注释前面(VS Code 按组名的字母序排组,组和组之间画分隔线)', () => {
it('字和 VS Code 自带的一样,只在右键菜单里,不挂行尾按钮,命令面板里藏起来', () => {
for (const { command, title } of REVEALS) {
expect(titleOf(command)?.title).toBe(title)
expect(menus['view/item/context'].filter(item => item.command === command)).toHaveLength(1)
expect(palette.get(command)).toBe('false')
}
})
it('右键菜单分组:复制、在系统文件管理器中显示、注释、收藏目录(VS Code 按组名的字母序排组,组和组之间画分隔线)', () => {
const groups = menus['view/item/context']
.filter(item => item.when.includes('view == superpowers.codeReader.files') && item.group !== undefined && !item.group.startsWith('inline'))
.filter(item => item.group !== undefined && !item.group.startsWith('inline'))
.map(item => item.group!.split('@')[0])
expect([...new Set(groups)].sort((a, b) => a.localeCompare(b))).toEqual(['copy', 'fileManager', 'notes'])
})
it('只在右键菜单里,不挂行尾按钮,命令面板里藏起来', () => {
for (const id of REVEAL_IDS) {
expect(menus['view/item/context'].filter(item => item.command === id)).toHaveLength(1)
expect(menus.commandPalette.find(item => item.command === id)?.when).toBe('false')
}
expect([...new Set(groups)].sort((a, b) => a.localeCompare(b))).toEqual(['copy', 'fileManager', 'notes', 'star'])
})
})
describe('筛选(审查工单、未提交的更改)', () => {
const FILTER = 'superpowers.codeReader.filter'
describe('主栏的审查工单', () => {
const PICK = 'superpowers.codeReader.pickReviewIssue'
const UNCOMMITTED = 'superpowers.codeReader.showUncommitted'
const HIDE_TESTS = 'superpowers.codeReader.hideTests'
const SHOW_TESTS = 'superpowers.codeReader.showTests'
const HIDE_TESTS = 'superpowers.codeReader.hideReviewTests'
const SHOW_TESTS = 'superpowers.codeReader.showReviewTests'
const SHOW_ALL = 'superpowers.codeReader.showAllFiles'
const TESTS_HIDDEN = 'superpowers.codeReader.testsHidden'
const title = menus['view/title']
.filter(item => item.when.startsWith('view == superpowers.codeReader.files'))
.sort((a, b) => orderOf(a) - orderOf(b))
const whenOf = (command: string): string | undefined => title.find(item => item.command === command)?.when
const REFRESH = 'superpowers.codeReader.refresh'
const TESTS_HIDDEN = 'superpowers.codeReader.reviewTestsHidden'
it('标题栏:选工单、未提交的更改、隐藏 / 显示测试文件、开着筛选时才有的「显示全部」、刷新', () => {
expect(title.map(item => item.command)).toEqual([PICK, UNCOMMITTED, HIDE_TESTS, SHOW_TESTS, SHOW_ALL, 'superpowers.codeReader.refresh'])
expect(title.every(item => item.group?.startsWith('navigation'))).toBe(true)
expect(whenOf(SHOW_ALL)).toBe(`view == superpowers.codeReader.files && ${FILTER}`)
for (const command of [PICK, UNCOMMITTED])
expect(whenOf(command)).not.toContain(FILTER)
it('标题栏:平时是选工单、刷新;审查时多出隐藏 / 显示测试文件(按开关换着显示)和「显示全部」', () => {
expect(titleBar(FILES)).toEqual([PICK, REFRESH])
expect(titleBar(FILES, { [REVIEWING]: true })).toEqual([PICK, HIDE_TESTS, SHOW_ALL, REFRESH])
expect(titleBar(FILES, { [REVIEWING]: true, [TESTS_HIDDEN]: true })).toEqual([PICK, SHOW_TESTS, SHOW_ALL, REFRESH])
expect(menus['view/title'].filter(item => item.when.includes(FILES)).every(item => item.group?.startsWith('navigation'))).toBe(true)
})
it('隐藏 / 显示测试文件在未提交的更改和审查时都有(同一个开关),按是否隐藏着换着显示,整棵项目树上没有', () => {
const listing = `(${FILTER} == uncommitted || ${FILTER} == review)`
expect(whenOf(HIDE_TESTS)).toBe(`view == superpowers.codeReader.files && ${listing} && !${TESTS_HIDDEN}`)
expect(whenOf(SHOW_TESTS)).toBe(`view == superpowers.codeReader.files && ${listing} && ${TESTS_HIDDEN}`)
const palette = new Map(menus.commandPalette.map(item => [item.command, item.when]))
expect(palette.get(HIDE_TESTS)).toBe(`${listing} && !${TESTS_HIDDEN}`)
expect(palette.get(SHOW_TESTS)).toBe(`${listing} && ${TESTS_HIDDEN}`)
it('审查时主栏行上没有注释、收藏按钮(那些行是工作副本里的,不记注释、不收藏);别的栏照常有,复制照旧', () => {
const reviewing = { [REVIEWING]: true }
expect(itemsFor('view/item/context', 'inline', file, FILES, reviewing)).toEqual([ASK, COPY_MENU])
expect(itemsFor('view/item/context', 'inline', folder, FILES, reviewing)).toEqual([ASK, COPY_MENU])
for (const group of ['notes', 'star'])
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])
})
it('「暂存」只在未提交的更改时挂在「更改」组的文件行上,命令面板里藏起来', () => {
const stage = menus['view/item/context'].filter(item => item.command === STAGE)
expect(stage).toHaveLength(1)
expect(stage[0].when).toContain(`${FILTER} == uncommitted`)
expect(menus.commandPalette.find(item => item.command === STAGE)?.when).toBe('false')
it('命令面板:「显示全部」、隐藏 / 显示测试文件只在审查时有', () => {
expect(evaluate(palette.get(SHOW_ALL)!, {})).toBe(false)
expect(evaluate(palette.get(SHOW_ALL)!, { [REVIEWING]: true })).toBe(true)
expect(evaluate(palette.get(HIDE_TESTS)!, { [REVIEWING]: true })).toBe(true)
expect(evaluate(palette.get(HIDE_TESTS)!, {})).toBe(false)
expect(evaluate(palette.get(SHOW_TESTS)!, { [REVIEWING]: true, [TESTS_HIDDEN]: true })).toBe(true)
})
it('按钮的字和图标', () => {
expect(commands.find(item => item.command === PICK)).toMatchObject({ title: '选工单', icon: '$(git-pull-request)' })
expect(commands.find(item => item.command === UNCOMMITTED)).toMatchObject({ title: '未提交的更改', icon: '$(git-commit)' })
expect(commands.find(item => item.command === HIDE_TESTS)).toMatchObject({ title: '隐藏测试文件', icon: '$(beaker-stop)' })
expect(commands.find(item => item.command === SHOW_TESTS)).toMatchObject({ title: '显示测试文件', icon: '$(beaker)' })
expect(commands.find(item => item.command === STAGE)).toMatchObject({ title: '暂存', icon: '$(add)' })
expect(commands.find(item => item.command === SHOW_ALL)).toMatchObject({ title: '显示全部', icon: '$(close)' })
expect(titleOf(PICK)).toMatchObject({ title: '选工单', icon: '$(git-pull-request)' })
expect(titleOf(HIDE_TESTS)).toMatchObject({ title: '隐藏测试文件', icon: '$(beaker-stop)' })
expect(titleOf(SHOW_TESTS)).toMatchObject({ title: '显示测试文件', icon: '$(beaker)' })
expect(titleOf(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(`${FILTER} != review`))).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(FILTER))).toBe(false)
describe('「未提交的更改」一栏', () => {
const HIDE_TESTS = 'superpowers.codeReader.hideUncommittedTests'
const SHOW_TESTS = 'superpowers.codeReader.showUncommittedTests'
const REFRESH = 'superpowers.codeReader.refreshUncommitted'
const TESTS_HIDDEN = 'superpowers.codeReader.uncommittedTestsHidden'
it('是主栏下面常驻的一栏', () => {
expect(views['superpowers-reader'].slice(0, 2)).toEqual([
{ id: FILES, name: '代码阅读' },
{ id: UNCOMMITTED, name: '未提交的更改' },
])
})
it('标题栏:隐藏 / 显示测试文件(这一栏自己的开关,和审查的分开)、刷新;不跟着审查变', () => {
expect(titleBar(UNCOMMITTED)).toEqual([HIDE_TESTS, REFRESH])
expect(titleBar(UNCOMMITTED, { [TESTS_HIDDEN]: true })).toEqual([SHOW_TESTS, REFRESH])
expect(titleBar(UNCOMMITTED, { [REVIEWING]: true, 'superpowers.codeReader.reviewTestsHidden': true })).toEqual([HIDE_TESTS, REFRESH])
})
it('按钮的字和图标同主栏的,命令面板里藏起来(只对这一栏)', () => {
expect(titleOf(HIDE_TESTS)).toMatchObject({ title: '隐藏测试文件', icon: '$(beaker-stop)' })
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(palette.get(id)).toBe('false')
})
})
describe('收藏栏', () => {
it(`预先声明 ${FAVORITE_SLOTS.length} 个栏位,排在「未提交的更改」后面,各自有收藏占着时才显示`, () => {
expect(views['superpowers-reader'].slice(2)).toEqual(FAVORITE_SLOTS.map(slot => ({ id: favoriteViewId(slot), name: `收藏 ${slot}`, when: favoriteContext(slot) })))
})
it('每一栏的标题栏:重命名、取消收藏,只对这一栏', () => {
for (const slot of FAVORITE_SLOTS)
expect(titleBar(favoriteViewId(slot))).toEqual([favoriteCommand('rename', slot), favoriteCommand('remove', slot)])
})
it('每个栏位的命令都登记了、字和图标一样,命令面板里藏起来', () => {
for (const slot of FAVORITE_SLOTS) {
expect(titleOf(favoriteCommand('rename', slot))).toMatchObject({ title: '重命名', icon: '$(edit)' })
expect(titleOf(favoriteCommand('remove', slot))).toMatchObject({ title: '取消收藏', icon: '$(star-full)' })
expect(palette.get(favoriteCommand('rename', slot))).toBe('false')
expect(palette.get(favoriteCommand('remove', slot))).toBe('false')
}
})
it('命令面板里「显示全部」只在开着筛选时有', () => {
expect(menus.commandPalette.find(item => item.command === SHOW_ALL)?.when).toBe(FILTER)
it('「收藏目录」:行尾按钮和右键菜单同一项,只对目录行(不含根、文件),星标图标,命令面板里藏起来', () => {
for (const view of [FILES, FAVORITE]) {
expect(itemsFor('view/item/context', 'star', folder, view)).toEqual([ADD_FAVORITE])
for (const context of [file, root, unstaged])
expect(itemsFor('view/item/context', 'star', context, view)).toEqual([])
}
expect(itemsFor('view/item/context', 'star', folder, UNCOMMITTED)).toEqual([])
expect(titleOf(ADD_FAVORITE)).toMatchObject({ title: '收藏目录', icon: '$(star-empty)' })
expect(palette.get(ADD_FAVORITE)).toBe('false')
})
})
it('只用一个上下文键,旧的 superpowers.codeReader.reviewing 不再出现', () => {
describe('菜单里用到的', () => {
it('命令都登记了;旧的筛选按钮、上下文键不再出现', () => {
const ids = new Set(commands.map(item => item.command))
for (const item of Object.values(menus).flat().filter(item => item.command?.startsWith('superpowers.codeReader.')))
expect(ids.has(item.command!)).toBe(true)
expect(ids.has('superpowers.codeReader.showUncommitted')).toBe(false)
const whens = Object.values(menus).flat().map(item => item.when)
expect(whens.some(when => when.includes('superpowers.codeReader.reviewing'))).toBe(false)
expect(whens.some(when => /superpowers\.codeReader\.(?:filter|testsHidden)\b/.test(when))).toBe(false)
})
})