✨ feat(vscode): 代码阅读「未提交的更改」点文件行改成打开标准预览(和主栏一样不标差异),文件行上加「打开更改」按钮才打开对比上一次提交的预览,语言没有代码阅读的走 VS Code 自带的差异视图,删掉的文件行不挂这个按钮
This commit is contained in:
1 parent
4b6e110a44
commit
c2a13e5691
18 files changed
+211
-79
No files matched your search
@@ -25,7 +25,7 @@ function source(root: string, label: string): DiffBaseSource & { fire: () => voi
|
||||
}
|
||||
}
|
||||
|
||||
describe('预览对比什么:审查按位置,上一次提交看是不是从「未提交的更改」点开的', () => {
|
||||
describe('预览对比什么:审查按位置,上一次提交看是不是用「打开更改」打开的', () => {
|
||||
const review = source('/worktrees/issue-446', 'PR 目标分支 master')
|
||||
const head = source('/ws', '上一次提交')
|
||||
const bases = previewDiffBases(review, head)
|
||||
@@ -35,7 +35,7 @@ describe('预览对比什么:审查按位置,上一次提交看是不是从
|
||||
expect(bases.diffBase(uri('/worktrees/issue-446/src/a.ts'), 'other')?.label).toBe('PR 目标分支 master')
|
||||
})
|
||||
|
||||
it('工作区仓库里有改动的文件:从「未提交的更改」点开的对比上一次提交,别处打开的不比', () => {
|
||||
it('工作区仓库里有改动的文件:用「未提交的更改」文件行上的「打开更改」打开的对比上一次提交,别处打开的(含点这一栏的文件行)不比', () => {
|
||||
expect(bases.diffBase(uri('/ws/src/a.ts'), 'uncommitted')?.label).toBe('上一次提交')
|
||||
expect(bases.diffBase(uri('/ws/src/a.ts'), 'other')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Disposable } from 'vscode'
|
||||
|
||||
/**
|
||||
* 对比基准:预览拿它取这个文件改之前的样子,和现在的比出差异。
|
||||
* 给哪个基准见 [previewDiffBases]:审查工作副本里的对比 PR 的目标分支,从「未提交的更改」一栏点开的有改动的文件对比上一次提交。
|
||||
* 给哪个基准见 [previewDiffBases]:审查工作副本里的对比 PR 的目标分支,用「未提交的更改」文件行上的「打开更改」打开的有改动的文件对比上一次提交。
|
||||
*/
|
||||
export interface DiffBase {
|
||||
/** 筛选栏上「对比:」后面的字:「上一次提交」「PR 目标分支 master」 */
|
||||
@@ -22,7 +22,10 @@ export interface DiffBaseSource {
|
||||
readonly onDidChange: Event<void>
|
||||
}
|
||||
|
||||
/** 一个预览标签页是从哪儿打开的:从侧栏「未提交的更改」一栏点开的是 `uncommitted`,别处(主栏、收藏栏、点链接、跳定义……)打开的是 `other`。 */
|
||||
/**
|
||||
* 一个预览标签页是从哪儿打开的:用侧栏「未提交的更改」文件行上的「打开更改」打开的是 `uncommitted`,
|
||||
* 别处(点这一栏的文件行、主栏、收藏栏、点链接、跳定义……)打开的是 `other`。
|
||||
*/
|
||||
export type OpenedFrom = 'uncommitted' | 'other'
|
||||
|
||||
/** 预览问对比基准的地方:同一个文件从哪儿打开的不一样,对比的也可能不一样;基准变了发 onDidChange,打开着的预览重新问。 */
|
||||
@@ -32,7 +35,7 @@ export interface PreviewDiffBases {
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览对比什么:审查工作副本里的文件不管从哪儿打开都对比 PR 的目标分支,其余的只有从「未提交的更改」一栏点开的才对比上一次提交,别处打开的不比。
|
||||
* 预览对比什么:审查工作副本里的文件不管从哪儿打开都对比 PR 的目标分支,其余的只有用「未提交的更改」文件行上的「打开更改」打开的才对比上一次提交,别处打开的不比。
|
||||
*
|
||||
* @param review 审查:工作副本里的文件给 PR 目标分支
|
||||
* @param uncommitted 工作区仓库里有改动的文件给上一次提交
|
||||
|
||||
@@ -208,7 +208,7 @@ export interface StickyScrollSettings {
|
||||
* openFind:打开查找栏,由「查找」命令发给活动的预览
|
||||
* moveFind:查找栏里跳到下一处(step 1)/ 上一处(step -1)命中,由「查找下一处 / 上一处」命令发给查找栏开着的预览
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
|
||||
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
|
||||
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Uri } from 'vscode'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Range, workspace } from 'vscode'
|
||||
import { languageOf, linesSpot, openingFor } from './openTarget'
|
||||
import type { PreviewEditorProvider } from './previewEditor'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { commands, Range, workspace } from 'vscode'
|
||||
import { languageOf, linesSpot, openFile, openingFor } from './openTarget'
|
||||
|
||||
vi.mock('vscode', () => ({
|
||||
commands: { executeCommand: vi.fn() },
|
||||
workspace: { openTextDocument: vi.fn() },
|
||||
Range: class {
|
||||
readonly start: { line: number, character: number }
|
||||
@@ -77,3 +79,58 @@ describe('文件的语言', () => {
|
||||
expect(await languageOf({} as Uri)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('侧栏打开文件', () => {
|
||||
const uri = { path: '/ws/src/a' } as Uri
|
||||
const executeCommand = vi.mocked(commands.executeCommand)
|
||||
const previews = { open: vi.fn<PreviewEditorProvider['open']>(async () => {}) }
|
||||
|
||||
/** VS Code 把这个文件认成 languageId;undefined 是打不开成文本(图片等) */
|
||||
function languageIs(languageId: string | undefined): void {
|
||||
const openTextDocument = vi.mocked(workspace.openTextDocument)
|
||||
if (languageId === undefined)
|
||||
openTextDocument.mockRejectedValueOnce(new Error('binary'))
|
||||
else
|
||||
openTextDocument.mockResolvedValueOnce({ languageId } as Awaited<ReturnType<typeof workspace.openTextDocument>>)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
executeCommand.mockClear()
|
||||
previews.open.mockClear()
|
||||
})
|
||||
|
||||
it('「打开更改」:语言有代码阅读的用预览打开,带上 uncommitted(预览对比上一次提交)', async () => {
|
||||
languageIs('python')
|
||||
await openFile(uri, 'uncommitted', supports, previews)
|
||||
expect(previews.open).toHaveBeenCalledWith(uri, { from: 'uncommitted' })
|
||||
expect(executeCommand).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('「打开更改」:预览打不开的(语言没有代码阅读、打不开成文本)用 VS Code 自带的差异视图', async () => {
|
||||
for (const languageId of ['c', undefined]) {
|
||||
languageIs(languageId)
|
||||
await openFile(uri, 'uncommitted', supports, previews)
|
||||
expect(executeCommand).toHaveBeenLastCalledWith('git.openChange', uri)
|
||||
}
|
||||
expect(previews.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('点文件行:语言有代码阅读的用预览打开、带上 other(不标差异),没有的用 VS Code 默认的打开方式', async () => {
|
||||
languageIs('python')
|
||||
await openFile(uri, 'other', supports, previews)
|
||||
expect(previews.open).toHaveBeenCalledWith(uri, { from: 'other' })
|
||||
languageIs('c')
|
||||
await openFile(uri, 'other', supports, previews)
|
||||
expect(executeCommand).toHaveBeenCalledWith('vscode.open', uri, undefined)
|
||||
expect(executeCommand).not.toHaveBeenCalledWith('git.openChange', expect.anything())
|
||||
})
|
||||
|
||||
it('给了位置:预览滚过去、闪这处定义,普通编辑器选中它', async () => {
|
||||
languageIs('python')
|
||||
await openFile(uri, 'other', supports, previews, spot)
|
||||
expect(previews.open).toHaveBeenCalledWith(uri, { from: 'other', reveal: { line: 41, definition: 1200 } })
|
||||
languageIs('c')
|
||||
await openFile(uri, 'other', supports, previews, spot)
|
||||
expect(executeCommand).toHaveBeenCalledWith('vscode.open', uri, { selection: range })
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Uri } from 'vscode'
|
||||
import type { OpenedFrom } from '../diff/diffBase'
|
||||
import type { LineRange, Reveal } from './messages'
|
||||
import { Range, workspace } from 'vscode'
|
||||
import type { PreviewEditorProvider } from './previewEditor'
|
||||
import { commands, Range, workspace } from 'vscode'
|
||||
|
||||
/**
|
||||
* 别的文件里要打开的一处:普通编辑器选中 `range`;预览按 `offset`(定义处的下标)滚过去、闪这处定义。
|
||||
@@ -29,6 +31,29 @@ export function openingFor(languageId: string | undefined, supports: (languageId
|
||||
return { kind: 'default', ...(spot && { selection: spot.range }) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏打开一个文件(点文件行、点「打开更改」、点「了解一块代码」栏里的一条):语言受支持的用预览打开,告诉预览是从哪儿打开的;
|
||||
* 预览打不开的(见 [openingFor]),从「打开更改」来的用 VS Code 自带的差异视图看这个文件的改动,别处来的用 VS Code 默认的打开方式。
|
||||
*
|
||||
* @param uri 要打开的文件
|
||||
* @param from 从哪儿打开的:`uncommitted` 时预览标出和上一次提交的差异
|
||||
* @param supports 这种语言有没有代码阅读
|
||||
* @param previews 语言受支持时用它打开预览
|
||||
* @param spot 要跳到的那一处(预览滚过去、闪一下,普通编辑器选中它);只打开文件时没有
|
||||
*/
|
||||
export async function openFile(uri: Uri, from: OpenedFrom, supports: (languageId: string) => boolean, previews: Pick<PreviewEditorProvider, 'open'>, spot?: TargetSpot): Promise<void> {
|
||||
const opening = openingFor(await languageOf(uri), supports, spot)
|
||||
if (opening.kind === 'preview') {
|
||||
await previews.open(uri, { from, ...(opening.reveal && { reveal: opening.reveal }) })
|
||||
return
|
||||
}
|
||||
if (from === 'uncommitted') {
|
||||
await commands.executeCommand('git.openChange', uri)
|
||||
return
|
||||
}
|
||||
await commands.executeCommand('vscode.open', uri, opening.selection && { selection: opening.selection })
|
||||
}
|
||||
|
||||
/** 预览怎么滚到这一处:有 `offset` 的跳到定义,没有的跳到 `range` 那几行。 */
|
||||
export function revealOf({ range, offset }: TargetSpot): Reveal {
|
||||
return offset === undefined ? { line: range.start.line, endLine: range.end.line } : { line: range.start.line, definition: offset }
|
||||
|
||||
@@ -197,7 +197,7 @@ function themeColors(colors: CodeColors) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 对比基准只认从「未提交的更改」点开的:从那儿点开的对比上一次提交,别处打开的不比 */
|
||||
/** 对比基准只认用「未提交的更改」文件行上的「打开更改」打开的:这样打开的对比上一次提交,别处打开的不比 */
|
||||
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, colors = themeColors(COLORS), languageFor: (languageId: string) => unknown = () => undefined) {
|
||||
const subscribe = () => ({ dispose: () => {} })
|
||||
const diffBase = vi.fn((_uri: unknown, from: OpenedFrom): DiffBase | undefined => from === 'uncommitted' ? HEAD_BASE : undefined)
|
||||
@@ -276,7 +276,7 @@ function edit(document: { version: number }, version: number): void {
|
||||
const comparedWithHead = expect.objectContaining({ type: 'diff', diff: expect.objectContaining({ base: '上一次提交' }) })
|
||||
|
||||
describe('预览对比什么看这个标签页是从哪儿打开的', () => {
|
||||
it('从「未提交的更改」点开的新预览对比上一次提交,页面记下是从哪儿打开的', async () => {
|
||||
it('用「打开更改」打开的新预览对比上一次提交,页面记下是从哪儿打开的', async () => {
|
||||
const { provider, document } = opening()
|
||||
const page = tab()
|
||||
await provider.open(document.uri as never, { from: 'uncommitted' }, async () => provider.resolveCustomTextEditor(document as never, page.panel))
|
||||
@@ -307,7 +307,7 @@ describe('预览对比什么看这个标签页是从哪儿打开的', () => {
|
||||
expect(diffBase).not.toHaveBeenCalledWith(document.uri, 'uncommitted')
|
||||
})
|
||||
|
||||
it('开着的预览再从主栏点开就不比,再从「未提交的更改」点开又比;页面重新加载后按它记着的照旧比', async () => {
|
||||
it('开着的预览再点文件行打开就不比,再点「打开更改」又比;页面重新加载后按它记着的照旧比', async () => {
|
||||
const { provider, document } = opening()
|
||||
const page = tab()
|
||||
await provider.open(document.uri as never, { from: 'uncommitted' }, async () => provider.resolveCustomTextEditor(document as never, page.panel))
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ export class FavoriteViews {
|
||||
/**
|
||||
* @param state workspaceState
|
||||
* @param pool 和「了解一块代码」共用的栏位
|
||||
* @param openCommand 点文件时执行的命令,参数是文件 Uri 和 `other`(不是从「未提交的更改」点开的)
|
||||
* @param openCommand 点文件时执行的命令,参数是文件 Uri 和 `other`:打开的预览不标差异
|
||||
* @param decorator 给行添信息(文件注释),和别的栏共用
|
||||
*/
|
||||
constructor(private readonly state: Memento, private readonly pool: SlotPool, private readonly openCommand: string, private readonly decorator: FileTreeDecorator) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ExtensionContext, Uri } from 'vscode'
|
||||
import type { CodeReaderApi, RuleLocation, RuleReference } from './contract'
|
||||
import type { OpenedFrom } from './diff/diffBase'
|
||||
import type { TargetSpot } from './editor/openTarget'
|
||||
import type { FileNode } from './explorer/fileTree'
|
||||
import type { FeatureRow } from './features/featureViews'
|
||||
import type { IndexedName } from './nameResolution'
|
||||
@@ -14,7 +13,7 @@ import { previewDiffBases } from './diff/diffBase'
|
||||
import { BlockFilter } from './editor/blockFilter'
|
||||
import { FIND, FIND_NEXT, FIND_PREVIOUS } from './editor/findCommands'
|
||||
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
|
||||
import { languageOf, openingFor } from './editor/openTarget'
|
||||
import { languageOf, openFile } from './editor/openTarget'
|
||||
import { PATH_PREVIEW_SCHEME } from './editor/pathPreview'
|
||||
import { PreviewEditorProvider } from './editor/previewEditor'
|
||||
import { EditorSwitch } from './editor/switchEditor'
|
||||
@@ -73,26 +72,14 @@ const LANGUAGE_IDS_CONTEXT = 'superpowers.codeReader.languageIds'
|
||||
const FILES_VIEW = 'superpowers.codeReader.files'
|
||||
const OPEN_COMMAND = 'superpowers.codeReader.open'
|
||||
|
||||
/**
|
||||
* 侧栏点文件、点「了解一块代码」栏里的一条:语言受支持就用预览打开、告诉它是从哪一栏点开的(`from`),否则用 VS Code 默认的打开方式(见 [openingFor])。
|
||||
* 给了 `spot`(条目的符号在哪)就跳过去:预览滚到那一块、闪一下,普通编辑器选中那个名字。
|
||||
*/
|
||||
async function openFile(uri: Uri, from: OpenedFrom, registry: CodeReaderRegistry, previews: PreviewEditorProvider, spot?: TargetSpot): Promise<void> {
|
||||
const opening = openingFor(await languageOf(uri), languageId => registry.languageFor(languageId) !== undefined, spot)
|
||||
if (opening.kind === 'default') {
|
||||
await commands.executeCommand('vscode.open', uri, opening.selection && { selection: opening.selection })
|
||||
return
|
||||
}
|
||||
await previews.open(uri, { from, ...(opening.reveal && { reveal: opening.reveal }) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 接入代码阅读:注册表、侧栏的几栏(主栏的文件树可以按审查工单只看 PR 改过的文件,常驻的「未提交的更改」,每个收藏目录、每个「了解一块代码」找的功能各一栏;
|
||||
* 行上有问路径、复制、在系统文件管理器里显示、文件注释、收藏目录)、预览编辑器(含问 AI,审查工作副本里的和从「未提交的更改」点开的标差异)、编辑器右上角的「预览 / 编辑」切换。
|
||||
* 行上有问路径、复制、在系统文件管理器里显示、文件注释、收藏目录)、预览编辑器(含问 AI,审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的标差异)、编辑器右上角的「预览 / 编辑」切换。
|
||||
* 内置的语言插件、翻译插件、库规则和命令规则与其他扩展一样通过返回的 API 注册。
|
||||
*/
|
||||
export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
const registry = new CodeReaderRegistry(context.globalState)
|
||||
const supports = (languageId: string): boolean => registry.languageFor(languageId) !== undefined
|
||||
const runtime = new TreeSitterRuntime(bundledWasmDir())
|
||||
const report = (message: string): void => logger.add({ level: 'warn', source: 'codeReader', message })
|
||||
// 有语言插件的按它的语法包解析,没有的(接口契约 openapi.json)用索引专用的语法包
|
||||
@@ -121,7 +108,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
const favorites = new FavoriteViews(context.workspaceState, slots, OPEN_COMMAND, decorator)
|
||||
const features = new FeatureViews(context.workspaceState, slots, qa, item => locateItem(item, { adapterFor: languageId => registry.languageFor(languageId), runtime }))
|
||||
const heads = new HeadBases()
|
||||
// 预览对比什么:审查工作副本里的对比 PR 的目标分支,从「未提交的更改」一栏点开的有改动的文件对比上一次提交,别处打开的不比
|
||||
// 预览对比什么:审查工作副本里的对比 PR 的目标分支,用「未提交的更改」文件行上的「打开更改」打开的有改动的文件对比上一次提交,别处打开的(含点这一栏的文件行)不比
|
||||
const diffBases = previewDiffBases(review, heads)
|
||||
// 预览里代码的上色、卡片的种类色跟着当前颜色主题走
|
||||
const codeColors = new ThemeColors(readCodeColors)
|
||||
@@ -158,7 +145,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
registerPathAsk(
|
||||
async (uri) => {
|
||||
const languageId = await languageOf(uri)
|
||||
return languageId !== undefined && registry.languageFor(languageId) !== undefined
|
||||
return languageId !== undefined && supports(languageId)
|
||||
},
|
||||
uri => previews.open(uri, { focusPath: true }),
|
||||
),
|
||||
@@ -188,7 +175,8 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
commands.registerCommand('superpowers.codeReader.refreshUncommitted', () => uncommitted.reload()),
|
||||
commands.registerCommand('superpowers.codeReader.hideUncommittedTests', () => uncommitted.setTestsHidden(true)),
|
||||
commands.registerCommand('superpowers.codeReader.showUncommittedTests', () => uncommitted.setTestsHidden(false)),
|
||||
// 「更改」组文件行上的按钮传来的是这一行的节点
|
||||
// 「未提交的更改」文件行上的按钮传来的是这一行的节点:「打开更改」两组都有,「暂存」只在「更改」组
|
||||
commands.registerCommand('superpowers.codeReader.openChanges', (node?: FileNode) => node && openFile(node.uri, 'uncommitted', supports, previews)),
|
||||
commands.registerCommand('superpowers.codeReader.stage', (node?: FileNode) => uncommitted.stage(node)),
|
||||
// 目录行上的按钮传来的是这一行的节点;栏标题栏上的按钮不带是哪一栏,所以每个栏位各一套命令
|
||||
commands.registerCommand('superpowers.codeReader.addFavorite', (node?: FileNode) => favorites.add(node)),
|
||||
@@ -201,12 +189,12 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
commands.registerCommand(featureCommand('remove', slot), () => features.remove(slot)),
|
||||
]),
|
||||
// 侧栏各栏的文件行传来的是 (文件 Uri, 从哪一栏点开的)
|
||||
commands.registerCommand(OPEN_COMMAND, (uri: Uri, from: OpenedFrom) => openFile(uri, from, registry, previews)),
|
||||
commands.registerCommand(OPEN_COMMAND, (uri: Uri, from: OpenedFrom) => openFile(uri, from, supports, previews)),
|
||||
// 「了解一块代码」栏里的条目行传来的是这一行:按眼下的代码找到它再打开
|
||||
commands.registerCommand(OPEN_FEATURE_ITEM, async (row?: FeatureRow) => {
|
||||
const found = await features.target(row)
|
||||
if (found)
|
||||
await openFile(found.uri, 'other', registry, previews, found.spot)
|
||||
await openFile(found.uri, 'other', supports, previews, found.spot)
|
||||
}),
|
||||
// 标题栏按钮传来的是 (文件 Uri, { groupId }),原样转给 VS Code 定位是哪一组的哪个标签
|
||||
commands.registerCommand('superpowers.codeReader.showPreview', (...args: unknown[]) => switcher.showPreview(args)),
|
||||
|
||||
@@ -13,7 +13,7 @@ function errorText(err: unknown): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作区仓库里有没提交改动的文件,对比上一次提交(见 [headDiffBase]);只有从「未提交的更改」一栏点开的预览来问(见 [previewDiffBases](vscode/src/codeReader/diff/diffBase.ts#previewDiffBases))。
|
||||
* 工作区仓库里有没提交改动的文件,对比上一次提交(见 [headDiffBase]);只有用「未提交的更改」文件行上的「打开更改」打开的预览来问(见 [previewDiffBases](vscode/src/codeReader/diff/diffBase.ts#previewDiffBases))。
|
||||
* 第一次有预览来问时才去读 git,读到了通知预览重新问;之后盯着 git 状态,改动、HEAD 变了都通知打开着的预览重新比。
|
||||
*/
|
||||
export class HeadBases implements DiffBaseSource, Disposable {
|
||||
|
||||
@@ -28,7 +28,7 @@ function errorText(err: unknown): string {
|
||||
|
||||
/**
|
||||
* 侧栏「未提交的更改」一栏:照源代码管理视图分「暂存的更改」「更改」两组(空的不摆),每组下面每个有改动的仓库一个根。
|
||||
* 文件就是工作区里的文件,颜色和 M / A / D 徽标由 VS Code 自带的 git 装饰加上;「更改」组里的文件行上有「暂存」按钮。
|
||||
* 文件就是工作区里的文件,颜色和 M / A / D 徽标由 VS Code 自带的 git 装饰加上;点文件行打开标准预览,要看改了什么点行上的「打开更改」(预览标出和上一次提交的差异,删掉的文件没有这个按钮),「更改」组里的文件行上还有「暂存」按钮。
|
||||
* 这一栏第一次露出来时才去读 git,之后盯着 git 状态自动重摆;隐藏测试文件的开关是这一栏自己的,按工作区记住。
|
||||
*/
|
||||
export class UncommittedView implements Disposable {
|
||||
@@ -43,11 +43,11 @@ export class UncommittedView implements Disposable {
|
||||
|
||||
/**
|
||||
* @param context 隐藏测试文件的开关记在它的 workspaceState 里
|
||||
* @param openCommand 点文件时执行的命令,参数是文件 Uri 和 `uncommitted`:从这一栏点开的预览对比上一次提交
|
||||
* @param openCommand 点文件行时执行的命令,参数是文件 Uri 和 `other`:和主栏点开的一样是标准预览,不标差异
|
||||
* @param decorator 给行添信息(文件注释),和别的栏共用
|
||||
*/
|
||||
constructor(context: ExtensionContext, openCommand: string, decorator: FileTreeDecorator) {
|
||||
this.files = new FileTreeProvider({ command: openCommand, from: 'uncommitted' }, decorator, { kind: 'changes', view: [] })
|
||||
this.files = new FileTreeProvider({ command: openCommand, from: 'other' }, decorator, { kind: 'changes', view: [] })
|
||||
this.view = window.createTreeView(UNCOMMITTED_VIEW, { treeDataProvider: this.files, showCollapseAll: true })
|
||||
this.tests = new TestsToggle(context.workspaceState, UNCOMMITTED_TESTS, () => this.rebuild())
|
||||
this.disposables = [this.files, this.view, whenShown(this.view, () => void this.open())]
|
||||
|
||||
Reference in new issue
Block a user