diff --git a/vscode/package.json b/vscode/package.json index 3fbe32f..159e53c 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -286,6 +286,12 @@ "category": "代码阅读", "icon": "$(beaker)" }, + { + "command": "superpowers.codeReader.openChanges", + "title": "打开更改", + "category": "代码阅读", + "icon": "$(git-compare)" + }, { "command": "superpowers.codeReader.stage", "title": "暂存", @@ -1381,7 +1387,7 @@ "superpowers.codeReader.copy": [ { "command": "superpowers.codeReader.copyFileName", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( deleted)?( noted)?$/", "group": "copy@1" }, { @@ -1391,17 +1397,17 @@ }, { "command": "superpowers.codeReader.copyFileStem", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( deleted)?( noted)?$/", "group": "copy@2" }, { "command": "superpowers.codeReader.copyRelativePath", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "copy@3" }, { "command": "superpowers.codeReader.copyAbsolutePath", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "copy@4" } ], @@ -2220,29 +2226,34 @@ "view/item/context": [ { "command": "superpowers.codeReader.askPath", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "inline@0" }, { "submenu": "superpowers.codeReader.copy", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "inline@1" }, { "command": "superpowers.codeReader.addNote", - "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?|folder)$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?( deleted)?|folder)$/", "group": "inline@2" }, { "command": "superpowers.codeReader.editNote", - "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?|folder) noted$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?( deleted)?|folder) noted$/", "group": "inline@2" }, { - "command": "superpowers.codeReader.stage", - "when": "view == superpowers.codeReader.uncommitted && viewItem =~ /^file unstaged( noted)?$/", + "command": "superpowers.codeReader.openChanges", + "when": "view == superpowers.codeReader.uncommitted && viewItem =~ /^file( unstaged)?( noted)?$/", "group": "inline@3" }, + { + "command": "superpowers.codeReader.stage", + "when": "view == superpowers.codeReader.uncommitted && viewItem =~ /^file unstaged( deleted)?( noted)?$/", + "group": "inline@4" + }, { "command": "superpowers.codeReader.addFavorite", "when": "view =~ /^superpowers\\.codeReader\\.(files|slot\\d+)$/ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^folder( noted)?$/", @@ -2250,7 +2261,7 @@ }, { "command": "superpowers.codeReader.copyFileName", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( deleted)?( noted)?$/", "group": "copy@1" }, { @@ -2260,42 +2271,42 @@ }, { "command": "superpowers.codeReader.copyFileStem", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^file( unstaged| feature)?( deleted)?( noted)?$/", "group": "copy@2" }, { "command": "superpowers.codeReader.copyRelativePath", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "copy@3" }, { "command": "superpowers.codeReader.copyAbsolutePath", - "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "copy@4" }, { "command": "superpowers.codeReader.revealInFinder", - "when": "view =~ /^superpowers\\.codeReader\\./ && isMac && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && isMac && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "fileManager" }, { "command": "superpowers.codeReader.revealInFileExplorer", - "when": "view =~ /^superpowers\\.codeReader\\./ && isWindows && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && isWindows && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "fileManager" }, { "command": "superpowers.codeReader.openContainingFolder", - "when": "view =~ /^superpowers\\.codeReader\\./ && isLinux && viewItem =~ /^(file( unstaged| feature)?|folder)( noted)?$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && isLinux && viewItem =~ /^(file( unstaged| feature)?( deleted)?|folder)( noted)?$/", "group": "fileManager" }, { "command": "superpowers.codeReader.addNote", - "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?|folder)$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?( deleted)?|folder)$/", "group": "notes" }, { "command": "superpowers.codeReader.editNote", - "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?|folder) noted$/", + "when": "view =~ /^superpowers\\.codeReader\\./ && !(view == superpowers.codeReader.files && superpowers.codeReader.reviewing) && viewItem =~ /^(file( unstaged)?( deleted)?|folder) noted$/", "group": "notes" }, { @@ -2333,6 +2344,10 @@ "command": "superpowers.codeReader.showReviewTests", "when": "superpowers.codeReader.reviewing && superpowers.codeReader.reviewTestsHidden" }, + { + "command": "superpowers.codeReader.openChanges", + "when": "false" + }, { "command": "superpowers.codeReader.stage", "when": "false" diff --git a/vscode/src/codeReader/diff/diffBase.test.ts b/vscode/src/codeReader/diff/diffBase.test.ts index ac710fc..a44cf2f 100644 --- a/vscode/src/codeReader/diff/diffBase.test.ts +++ b/vscode/src/codeReader/diff/diffBase.test.ts @@ -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() }) diff --git a/vscode/src/codeReader/diff/diffBase.ts b/vscode/src/codeReader/diff/diffBase.ts index 0710750..1d10ae0 100644 --- a/vscode/src/codeReader/diff/diffBase.ts +++ b/vscode/src/codeReader/diff/diffBase.ts @@ -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 } -/** 一个预览标签页是从哪儿打开的:从侧栏「未提交的更改」一栏点开的是 `uncommitted`,别处(主栏、收藏栏、点链接、跳定义……)打开的是 `other`。 */ +/** + * 一个预览标签页是从哪儿打开的:用侧栏「未提交的更改」文件行上的「打开更改」打开的是 `uncommitted`, + * 别处(点这一栏的文件行、主栏、收藏栏、点链接、跳定义……)打开的是 `other`。 + */ export type OpenedFrom = 'uncommitted' | 'other' /** 预览问对比基准的地方:同一个文件从哪儿打开的不一样,对比的也可能不一样;基准变了发 onDidChange,打开着的预览重新问。 */ @@ -32,7 +35,7 @@ export interface PreviewDiffBases { } /** - * 预览对比什么:审查工作副本里的文件不管从哪儿打开都对比 PR 的目标分支,其余的只有从「未提交的更改」一栏点开的才对比上一次提交,别处打开的不比。 + * 预览对比什么:审查工作副本里的文件不管从哪儿打开都对比 PR 的目标分支,其余的只有用「未提交的更改」文件行上的「打开更改」打开的才对比上一次提交,别处打开的不比。 * * @param review 审查:工作副本里的文件给 PR 目标分支 * @param uncommitted 工作区仓库里有改动的文件给上一次提交 diff --git a/vscode/src/codeReader/editor/messages.ts b/vscode/src/codeReader/editor/messages.ts index 8698436..0a675d8 100644 --- a/vscode/src/codeReader/editor/messages.ts +++ b/vscode/src/codeReader/editor/messages.ts @@ -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 是找不到的不带路径的引用(原样带回问的那几条) diff --git a/vscode/src/codeReader/editor/openTarget.test.ts b/vscode/src/codeReader/editor/openTarget.test.ts index 422f67c..500c91b 100644 --- a/vscode/src/codeReader/editor/openTarget.test.ts +++ b/vscode/src/codeReader/editor/openTarget.test.ts @@ -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(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>) + } + + 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 }) + }) +}) diff --git a/vscode/src/codeReader/editor/openTarget.ts b/vscode/src/codeReader/editor/openTarget.ts index 2654101..9e5db8a 100644 --- a/vscode/src/codeReader/editor/openTarget.ts +++ b/vscode/src/codeReader/editor/openTarget.ts @@ -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, spot?: TargetSpot): Promise { + 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 } diff --git a/vscode/src/codeReader/editor/previewEditor.test.ts b/vscode/src/codeReader/editor/previewEditor.test.ts index 127d0e3..5c55d53 100644 --- a/vscode/src/codeReader/editor/previewEditor.test.ts +++ b/vscode/src/codeReader/editor/previewEditor.test.ts @@ -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)) diff --git a/vscode/src/codeReader/explorer/fileEntries.test.ts b/vscode/src/codeReader/explorer/fileEntries.test.ts index 61c4445..d187fc8 100644 --- a/vscode/src/codeReader/explorer/fileEntries.test.ts +++ b/vscode/src/codeReader/explorer/fileEntries.test.ts @@ -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', () => { diff --git a/vscode/src/codeReader/explorer/fileEntries.ts b/vscode/src/codeReader/explorer/fileEntries.ts index f089bac..601376a 100644 --- a/vscode/src/codeReader/explorer/fileEntries.ts +++ b/vscode/src/codeReader/explorer/fileEntries.ts @@ -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(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 }` 只在有对应的兄弟文件时排除。 */ diff --git a/vscode/src/codeReader/explorer/fileTree.test.ts b/vscode/src/codeReader/explorer/fileTree.test.ts index 93b9a09..261ee6a 100644 --- a/vscode/src/codeReader/explorer/fileTree.test.ts +++ b/vscode/src/codeReader/explorer/fileTree.test.ts @@ -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 () => { diff --git a/vscode/src/codeReader/explorer/rowMenus.test.ts b/vscode/src/codeReader/explorer/rowMenus.test.ts index 18fb04b..6bffdaf 100644 --- a/vscode/src/codeReader/explorer/rowMenus.test.ts +++ b/vscode/src/codeReader/explorer/rowMenus.test.ts @@ -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') }) }) diff --git a/vscode/src/codeReader/favorites/favoriteViews.ts b/vscode/src/codeReader/favorites/favoriteViews.ts index 64ac884..f0641bc 100644 --- a/vscode/src/codeReader/favorites/favoriteViews.ts +++ b/vscode/src/codeReader/favorites/favoriteViews.ts @@ -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) { diff --git a/vscode/src/codeReader/index.ts b/vscode/src/codeReader/index.ts index cd0ff56..0264ba3 100644 --- a/vscode/src/codeReader/index.ts +++ b/vscode/src/codeReader/index.ts @@ -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 { - 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)), diff --git a/vscode/src/codeReader/uncommitted/headBases.ts b/vscode/src/codeReader/uncommitted/headBases.ts index 76ca5fa..8ea2f79 100644 --- a/vscode/src/codeReader/uncommitted/headBases.ts +++ b/vscode/src/codeReader/uncommitted/headBases.ts @@ -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 { diff --git a/vscode/src/codeReader/uncommitted/uncommittedView.ts b/vscode/src/codeReader/uncommitted/uncommittedView.ts index 863c1db..b36c887 100644 --- a/vscode/src/codeReader/uncommitted/uncommittedView.ts +++ b/vscode/src/codeReader/uncommitted/uncommittedView.ts @@ -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())] diff --git a/vscode/webview-ui/src/codeReader/BlockTree.tsx b/vscode/webview-ui/src/codeReader/BlockTree.tsx index 478f951..999bb3c 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.tsx @@ -61,7 +61,7 @@ export interface BlockTreeState { temporary: ReadonlySet /** 被正在编辑的祖先盖住、不画的块(它们的代码在祖先的编辑框里),见 coveredByEditing */ covered: ReadonlyMap - /** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */ + /** 代码差异:新增、修改的块和删掉的块;在审查工作副本里、或用「未提交的更改」文件行上的「打开更改」打开,且基准里有这个文件时才有 */ diff?: DiffTree /** 「只看改动」开着时折起来的连着没变的块(见 foldUnchanged) */ folded?: FoldedRuns diff --git a/vscode/webview-ui/src/codeReader/pageState.ts b/vscode/webview-ui/src/codeReader/pageState.ts index be8f986..0dcf51f 100644 --- a/vscode/webview-ui/src/codeReader/pageState.ts +++ b/vscode/webview-ui/src/codeReader/pageState.ts @@ -41,7 +41,7 @@ export interface PageInputs { excluded: Excluded /** 跳到定义时临时露出来的块(目标和它的各层祖先),筛选一变、文档一刷新就回到筛选状态 */ keep: ReadonlySet - /** 这份文件相比对比基准改了什么:审查工作副本里的、从「未提交的更改」点开的有改动的文件扩展端算好了补来,不比时是 null */ + /** 这份文件相比对比基准改了什么:审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的文件扩展端算好了补来,不比时是 null */ diff: DocumentDiff | null /** 「只看改动」开着(整个工作区共用,扩展端记着);本地不改,改了发给扩展端、等它发回来 */ onlyChanges: boolean diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index f36e91f..4e27d6b 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -497,7 +497,10 @@ export interface StickyScrollSettings { maxLineCount: number } -/** 这个标签页是从哪儿打开的:从侧栏「未提交的更改」一栏点开的是 `uncommitted`,别处打开的是 `other`;页面只记着,重启 VS Code 后恢复时报给扩展端。 */ +/** + * 这个标签页是从哪儿打开的:用侧栏「未提交的更改」文件行上的「打开更改」打开的是 `uncommitted`,别处(含点这一栏的文件行)打开的是 `other`; + * 页面只记着,重启 VS Code 后恢复时报给扩展端。 + */ export type OpenedFrom = 'uncommitted' | 'other' /** 代码上色的几种词性(codeReader.css 的 --cr-<词性>),源码和译文共用;operator 用 storage 的,literal 用 number 的 */ @@ -531,7 +534,7 @@ export interface CodeColors { * openFind:打开查找栏(⌘F / Ctrl+F),聚焦输入框;页面上选中了一行字时先填进去 * moveFind:查找栏里跳到下一处(step 1)/ 上一处(step -1)命中(⌘G / ⇧⌘G,Windows 是 F3 / Shift+F3) * displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理 - * diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了 + * diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了 * openedFrom:这个标签页是从哪儿打开的,记进页面状态;ready 后来一次,开着时又从侧栏点开、换了从哪儿打开的再来 * repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条) * repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,