✨ feat(vscode): 跳定义、点链接、编辑切回预览不改开着的预览对比什么

This commit is contained in:
cruldra committed 2026-10-12 16:10:41 +11:00
1 parent d4416e7b26
commit ce5327dcd5
8 files changed
+226 -34

No files matched your search

+6 -4
View File
@@ -224,8 +224,10 @@
审 AI 写的代码时,从只列改动的文件的地方(侧栏「未提交的更改」一栏、「选工单」审查)点开,预览的卡片标出这个文件相比改之前加了什么、改了什么、删了什么(原型插件菜单「代码差异」:画板「代码差异 · 全部 / 只看改动 / 标记说明」)。从主栏、收藏栏点开,点链接、跳定义这些别处打开的预览和平时一样,不标。
- 对比基准(`diff/diffBase.ts` 的 `DiffBase`,`previewDiffBases` 按文件在哪、这个标签页是从哪儿打开的给):
- 从侧栏「未提交的更改」一栏点开的、文件在工作区的仓库里有没提交的改动(`workspaceOrder`,审查的工作副本不算):对比上一次提交(HEAD),文案「对比:上一次提交」。从主栏、收藏栏点开,点仓库链接、⌘ / Ctrl + 点击跳到定义、点调用树、侧栏「问 AI」、编辑器右上角切回「预览」这些别处打开的不比。
- 从哪儿打开的是打开请求自带的(`editor/previewEditor.ts` 的 `OpenRequest.from`:`uncommitted` / `other`):侧栏文件行的打开命令带上这一栏的 `from`(`explorer/fileTree.ts` 的 `FileOpenCommand`),`PreviewEditorProvider.open` 把请求交给新建的预览;打开的是已经开着的预览时交给切到的那个,以最后一次从哪儿点开为准(从「未提交的更改」点开就比,再从主栏点开同一个文件就不比);只切标签页不变。页面把它记进 webview 状态,VS Code 重启后恢复的标签页没有打开请求,随 `ready` 报回来,照关掉前的比;页面也没记过的(VS Code 自己换编辑器打开的)不比。
- 从侧栏「未提交的更改」一栏点开的、文件在工作区的仓库里有没提交的改动(`workspaceOrder`,审查的工作副本不算):对比上一次提交(HEAD),文案「对比:上一次提交」。从主栏、收藏栏、「了解一块代码」栏点开的不比;点仓库链接、⌘ / Ctrl + 点击跳到定义、点调用树、侧栏「问 AI」新开的预览也不比。
- 从哪儿打开的是打开请求自带的(`editor/previewEditor.ts` 的 `OpenRequest.from`:`uncommitted` / `other`):只有侧栏各栏点开文件带上,文件行的打开命令带上这一栏的 `from`(`explorer/fileTree.ts` 的 `FileOpenCommand`),`PreviewEditorProvider.open` 把请求交给新建的预览;打开的是已经开着的预览时交给切到的那个,以最后一次从侧栏哪一栏点开为准(从「未提交的更改」点开就比,再从主栏点开同一个文件就不比)。跳定义、点链接、点调用树、「问 AI」只是去看一眼,不带 `from`:新开的预览不比,打到已经开着的预览照旧;只切标签页也不变。
- 编辑器右上角「编辑」再切回「预览」照切走前的比(`editor/switchEditor.ts` 的 `EditorSwitch`:切到文本编辑器时按编辑器组和文件记下预览是从哪儿打开的,切回时带上;那个文本编辑器标签页关了就忘掉,之后再点「预览」算新开的、不比)。
- 页面把它记进 webview 状态,VS Code 重启后恢复的标签页没有打开请求,随 `ready` 报回来,照关掉前的比;页面也没记过的(新开的、VS Code 自己换编辑器打开的)不比。
- 审查工单、文件在这张工单的工作副本里(不管从哪儿打开):对比 PR 目标分支与 PR 头的合并基(Gitea PR 的 `merge_base`,没给时退回目标分支的提交,同看板「改动」tab)上的版本,改名的取改名前的路径(`reviewBaseFile`);文案「对比:PR 目标分支 <base 分支名>」(PR 的 `base.ref`)。合并基是 PR 头的祖先,拉 PR 的提交时已经在本机,直接在工作副本里取。
- 取旧内容:`git/fileAt.ts` 的 `fileAtRef`(`git ls-tree` 看有没有这个文件、`git show <提交>:<路径>` 取内容,走 `runGit`);基准里没有这个文件(未跟踪、新加的、PR 里新加的、仓库还没有提交)不逐张标绿,顶部一条横幅「新文件,全部是新增」,后面淡色写原因(「还没被 git 跟踪,上一次提交里没有这个文件」「上一次提交里没有这个文件」「PR 目标分支 master 里没有这个文件」「仓库里还没有提交」)。
- 什么时候比(`diff/previewDiff.ts` 的 `PreviewDiff`,每个预览一个):每发出一份文档(编辑、保存、块编辑写回、换翻译)之后按眼下的基准重新比;基准挪了(盯着的 git 状态变了,比如提交后 HEAD 动了;审查进 / 重读工单)、开着的预览又被点开换了从哪儿打开的,打开着的预览也重新比。预览先照常画文档,差异算好了再补发(`diff` 消息),打开不变慢。取过的旧内容按基准的 key(提交 + 路径)记着,编辑时不用每次问 git;旧版本每次用同一个语言插件、翻译插件、库规则重新排成视图数据,规则查定义、调用的种类用这份文档眼下查到的答案,两边同样的写法翻得一样。取不到旧内容、旧版本解析失败时记日志、不标。
@@ -2080,7 +2082,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTab
- `{ type: 'answerEnd', askId, status: 'done' | 'failed' | 'stopped', text?, error?, entryId? }`:答完了。`done` 带最终回答和存下的那条的 id(历史里不再重复显示它),`failed` 带给人看的原因,`stopped` 是被叫停、没存。
- `{ type: 'rewriteProgress', anchor, question, text, activity? }`:`anchor` 那块正在重写注释:`question` 是发给模型的那一句,`text` 是正文到现在为止写出来的,`activity` 是正在读什么。`{ type: 'rewriteEnd', anchor, status: 'done' | 'failed' | 'stopped', error? }`:重写完了(见「重写注释」)。
- `{ type: 'repoLinksChecked', id, missing, unresolved }`:回答第 `id` 次 `checkRepoLinks`,`missing` 是仓库里没有的路径,`unresolved` 是找不到的不带路径的引用。`{ type: 'repoLinkBroken' | 'repoLinkOpened', path?, symbol? }`:点了以后打不开 / 打开了;没有 `path` 的是不带路径的引用。见「注释的 Markdown 排版」。
- `{ type: 'openedFrom', from: 'uncommitted' | 'other' }`:这个标签页是从哪儿打开的(见「代码差异」),页面记进 webview 状态。`ready` 后发一次,开着时又被点开再发。
- `{ type: 'openedFrom', from: 'uncommitted' | 'other' }`:这个标签页是从哪儿打开的(见「代码差异」),页面记进 webview 状态。`ready` 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发。
- webview → 扩展:
- `{ type: 'probeDefinition', id, version, offset }`:按住修饰键指到带 `offset` 的名字上时问有没有定义,`id` 每问一次加一;`version` 不是当前文档版本就不答。
- `{ type: 'goToDefinition', version, offset }`:修饰键 + 点击,按「跳到定义」一节跳。这两条都只带下标:扩展端按发出去的文档认出规则标了引用的下标交给规则查(见「规则自己查定义」),其余问语言扩展。
@@ -2309,7 +2311,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 代码差异:
- 块对齐和整棵树(`diff/blockDiff.test.ts`,真实的 Python 解析):原型的 `mutex.py`(改之前是编的)比出「新增 2 · 修改 3 · 删除 1」,新增块的子孙跟着算新增、只有最外面挂小标,`try_acquire` 自己改了、带属性表的标记,子块 `token` 改了、`ok` 和返回没变,`release` 只有子块改了(不算自己改了),删掉的 `_is_owner` 排在新增的 `renew` 后面、只带自己的内容、没有源码下标;没变的文件没有改动;子块里删掉的排在最前面时没有 after。对齐:有名字的按种类 + 名字配(挪了位置也配、同名照先后、种类不同不配、改名一删一增),没名字的按最长公共子序列和相似度配,种类不同不按相似度配。
- 卡片里的标记(`diff/viewDiff.test.ts`):只是源码下标不同不算改;大部分没变时逐截标、删掉的去掉下标,改得多整段旧值 → 新值;框按样式、框头、框里的词比;Markdown 按词、中文按字;一种文字换成另一种按字比;子行按第一列配(加行、删行放回原来的位置、改了的只标那一格、新加的行下面各层也是新加的);属性行按属性名配、子行改了不另标值;一行译文、摊开实参的表、说明各比各的;属性表和一行译文没法比。
- 对比什么看从哪儿打开的(`diff/diffBase.test.ts`、`editor/previewEditor.test.ts`、`explorer/fileTree.test.ts`、`viewState.test.ts`):审查工作副本里的不管从哪儿打开都对比 PR 目标分支,别的只有从「未提交的更改」点开的对比上一次提交;新开的预览按打开请求、开着的再被点开换成这次的(不比了发 null)、只切标签页不变;没有打开请求的按页面记着的、页面没记过的不比,打开请求比页面记着的优先;「未提交的更改」一栏的文件行命令带 `uncommitted`、别的栏带 `other`;页面记着从哪儿打开的、认不得的值当没记过。
- 对比什么看从哪儿打开的(`diff/diffBase.test.ts`、`editor/previewEditor.test.ts`、`explorer/fileTree.test.ts`、`viewState.test.ts`):审查工作副本里的不管从哪儿打开都对比 PR 目标分支,别的只有从「未提交的更改」点开的对比上一次提交;新开的预览按打开请求、开着的再从侧栏点开换成这次的(不比了发 null)、跳定义点链接打到开着的不变、新开的不比、只切标签页不变;「编辑」再切回「预览」照原来的,切过去的文本编辑器标签页关了就忘掉(`editor/switchEditor.test.ts`);没有打开请求的按页面记着的、页面没记过的不比,打开请求比页面记着的优先;「未提交的更改」一栏的文件行命令带 `uncommitted`、别的栏带 `other`;页面记着从哪儿打开的、认不得的值当没记过。
- 什么时候比(`diff/previewDiff.test.ts`):比完发出去(统计、删掉的块带种类名、一共几行);基准里没有这个文件是新文件;没有基准不发、发过后基准没了发一次 null;旧内容按 key 记着、key 变了重取;同一份文档同一个基准不再比;取不到旧内容记一笔、清掉、下次重取;慢的旧结果不盖掉新的。
- 取旧内容(`git/fileAt.test.ts`,真的 git):取的是提交里的、不是工作区改过的;提交里没有这个文件是 null;提交不存在抛错。对比基准的字(`uncommittedFiles.test.ts`、`reviewRules.test.ts`、`changedFiles.test.ts`):未跟踪的按状态认(两种 `git.untrackedChanges` 设置都认)、上一次提交里没有时的三种说明;PR 目标分支的文案;改名的取改名前的路径、PR 里新加的目标分支上没有。「只看改动」开关(`editor/blockFilter.test.ts`):记进 `workspaceState`、关了不记、改了发给所有预览带着眼下的块筛选。
- 页面(`diffView.test.ts`、`markdown.test.ts`、`viewState.test.ts`):块按记号认成页面上的 id、找不到的不标、不是这一版不画卡片里的标记,删掉的块按层挂、找不到挂处的和被筛掉的不画,新文件不逐块标,标了改动的默认展开;统计的字;删掉的块收起时的名字;只看改动折哪几段(改过的块下面按层折、删掉的块打断、被筛掉的不算、点开过的和要露出来的不折);各处改动的先后和行、被筛掉的不算;上一处 / 下一处接着走、按视口顶上的行找、到头绕回;带增删的 Markdown 照常排版、跨行逐行包、新加的列表项留住记号。
+1 -1
View File
@@ -136,7 +136,7 @@ export interface StickyScrollSettings {
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又被点开再发
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
@@ -218,7 +218,28 @@ describe('预览对比什么看这个标签页是从哪儿打开的', () => {
await vi.waitFor(() => expect(page.posted()).toContainEqual(comparedWithHead))
})
it('开着的预览再从别处点开就不比,再从「未提交的更改」点开又比;只切标签页不变', async () => {
it('跳定义、点链接打到开着的预览不改它对比什么、只滚过去;要新开的预览不比', async () => {
const { provider, document, diffBase } = opening()
const page = tab()
await provider.open(document.uri as never, { from: 'uncommitted' }, async () => provider.resolveCustomTextEditor(document as never, page.panel))
await page.send({ type: 'ready' })
await vi.waitFor(() => expect(page.posted()).toContainEqual(comparedWithHead))
page.clear()
await provider.open(document.uri as never, { reveal: { line: 1 } }, async () => {})
expect(page.posted()).toEqual([{ type: 'reveal', line: 1 }])
expect(provider.openedFrom(document.uri as never)).toBe('uncommitted')
diffBase.mockClear()
const fresh = tab()
await provider.open(document.uri as never, { reveal: { line: 1 } }, async () => provider.resolveCustomTextEditor(document as never, fresh.panel))
await fresh.send({ type: 'ready' })
expect(fresh.posted()).toContainEqual({ type: 'openedFrom', from: 'other' })
expect(diffBase).toHaveBeenCalledWith(document.uri, 'other')
expect(diffBase).not.toHaveBeenCalledWith(document.uri, 'uncommitted')
})
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))
+25 -7
View File
@@ -54,9 +54,11 @@ const REFRESH_DELAY_MS = 150
/**
* 一次打开预览带的话:这个标签页是从哪儿打开的(定对比什么);要滚到哪(从文本编辑器切过来时它顶部那一行,或跳到的定义);
* 要不要把光标放进顶部「问 AI」输入框(侧栏「问 AI」打开的)。
*
* 只有侧栏各栏点开文件、「编辑」切回「预览」带 `from`;跳定义、点链接、问 AI 这些只是去看一眼,不带:新开的预览不比,已经开着的照旧。
*/
export interface OpenRequest {
from: OpenedFrom
from?: OpenedFrom
reveal?: Reveal
focusPath?: boolean
}
@@ -149,9 +151,19 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
/** uri 的预览视口顶部那块的起始行;几个编辑器组里都开着这个文件的预览时取活动的那个。 */
visibleLine(uri: Uri): number | undefined {
return this.shown(uri)?.visibleLine
}
/** uri 的预览这个标签页是从哪儿打开的;几个编辑器组里都开着时取活动的那个,页面还没准备好、不知道的没有。 */
openedFrom(uri: Uri): OpenedFrom | undefined {
return this.shown(uri)?.openedFrom
}
/** uri 的预览:几个编辑器组里都开着时取活动的那个。 */
private shown(uri: Uri): Preview | undefined {
const key = uri.toString()
const previews = [...this.previews].filter(p => p.uri.toString() === key)
return (previews.find(p => p.active) ?? previews[0])?.visibleLine
return previews.find(p => p.active) ?? previews[0]
}
}
@@ -201,7 +213,7 @@ class Preview {
* 之后页面因标签页切走重新加载,按 webview 自己记的位置恢复
*/
private pendingReveal: Reveal | undefined
/** 这个标签页是从哪儿打开的,定对比什么;没带打开请求的(VS Code 重启后恢复的)先空着,页面 ready 时按它记着的定 */
/** 这个标签页是从哪儿打开的,定对比什么;打开请求没说的(跳定义新开的、VS Code 重启后恢复的)先空着,页面 ready 时按它记着的定 */
private from: OpenedFrom | undefined
constructor(
@@ -334,6 +346,10 @@ class Preview {
return this.panel.active
}
get openedFrom(): OpenedFrom | undefined {
return this.from
}
async push(): Promise<void> {
clearTimeout(this.timer)
if (!this.ready)
@@ -470,14 +486,16 @@ class Preview {
this.post({ type: 'moveOccurrence', step })
}
/** 开着时又被点开(见 [PreviewEditorProvider.open]):按这次是从哪儿打开的重新比,要滚到哪、要不要聚焦输入框照这次的。 */
/** 开着时又被点开(见 [PreviewEditorProvider.open]):这次说了从哪儿打开的就按它重新比,没说的照旧;要滚到哪、要不要聚焦输入框照这次的。 */
reopen(request: OpenRequest): void {
if (request.from !== undefined) {
this.from = request.from
if (this.ready) {
this.post({ type: 'openedFrom', from: request.from })
if (this.sent)
void this.diff.update(this.sent)
}
}
if (request.reveal)
this.reveal(request.reveal)
if (request.focusPath)
@@ -575,7 +593,7 @@ class Preview {
switch (msg.type) {
case 'ready':
this.ready = true
// 打开请求说了从哪儿打开的就按它;没说的(VS Code 重启后恢复的)按页面记着的,页面也没记过的不算从「未提交的更改」点开的
// 打开请求说了从哪儿打开的就按它;没说的按页面记着的(VS Code 重启后恢复的),页面也没记过的(新开的)不比
this.from ??= msg.openedFrom ?? 'other'
this.post({ type: 'openedFrom', from: this.from })
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
@@ -794,7 +812,7 @@ class Preview {
}
/**
* 在预览所在的编辑器组打开别的文件,和普通编辑器里 ⌘ + 点击一样:语言有代码阅读的用预览打开(算从别处打开的,不对比上一次提交),给了 spot 就滚过去、闪这处;
* 在预览所在的编辑器组打开别的文件,和普通编辑器里 ⌘ + 点击一样:语言有代码阅读的用预览打开(新开的不比,已经开着的照旧),给了 spot 就滚过去、闪这处;
* 其余用 VS Code 默认的打开方式,给了 spot 就选中它(见 openingFor)。目标是否按「预览标签」打开听 VS Code 的设置,
* workbench.editor.enablePreviewFromCodeNavigation 为假时先把当前这个预览标签固定住,免得被目标替掉。
*/
@@ -807,7 +825,7 @@ class Preview {
await commands.executeCommand('vscode.open', uri, { ...options, ...(opening.selection && { selection: opening.selection }) })
return
}
await this.owner.open(uri, { from: 'other', ...(opening.reveal && { reveal: opening.reveal }) }, () => commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, options))
await this.owner.open(uri, opening.reveal ? { reveal: opening.reveal } : {}, () => commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, options))
}
/** 点了调用树上的一个节点:打开它在的文件,滚到这一处(同 ⌘ + 点击跳到别的文件)。 */
@@ -0,0 +1,112 @@
import type { OpenedFrom } from '../diff/diffBase'
import type { OpenRequest } from './previewEditor'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EditorSwitch } from './switchEditor'
/** 假的 VS Code:文件、标签页的输入、活动的文本编辑器、标签页关掉的通知 */
const fake = vi.hoisted(() => {
class Uri {
constructor(readonly path: string) {}
toString(): string {
return `file://${this.path}`
}
}
class TabInputText {
constructor(readonly uri: Uri) {}
}
class TabInputCustom {
constructor(readonly uri: Uri, readonly viewType: string) {}
}
const tabListeners = new Set<(event: { closed: unknown[] }) => void>()
const window = {
activeTextEditor: undefined as unknown,
visibleTextEditors: [] as unknown[],
onDidChangeVisibleTextEditors: () => ({ dispose: () => {} }),
tabGroups: {
activeTabGroup: { activeTab: undefined },
onDidChangeTabs: (listener: (event: { closed: unknown[] }) => void) => {
tabListeners.add(listener)
return { dispose: () => tabListeners.delete(listener) }
},
},
}
return { Uri, TabInputText, TabInputCustom, window, tabListeners, executeCommand: vi.fn() }
})
vi.mock('vscode', () => ({
Uri: fake.Uri,
TabInputText: fake.TabInputText,
TabInputCustom: fake.TabInputCustom,
window: fake.window,
commands: { executeCommand: fake.executeCommand },
Range: class {
constructor(readonly startLine: number, readonly startCharacter: number, readonly endLine: number, readonly endCharacter: number) {}
},
TextEditorRevealType: { AtTop: 3 },
}))
vi.mock('./previewEditor', () => ({ PREVIEW_VIEW_TYPE: 'superpowers.codeReader.preview' }))
const uri = new fake.Uri('/workspace/web/a.ts')
/** 第 1 个编辑器组里 a.ts 的文本编辑器,顶部是第 10 行 */
const editor = { document: { uri, lineCount: 100 }, viewColumn: 1, visibleRanges: [{ start: { line: 10 } }], revealRange: vi.fn() }
/** 开着的预览:从 from 打开的;open 照 VS Code 那样执行打开 */
function previews(from: OpenedFrom | undefined) {
return {
visibleLine: () => 10,
openedFrom: () => from,
open: vi.fn(async (_uri: unknown, _request: OpenRequest, show: () => Thenable<unknown>) => {
await show()
}),
}
}
beforeEach(() => {
fake.window.activeTextEditor = undefined
fake.window.visibleTextEditors = []
fake.tabListeners.clear()
// 换成文本编辑器后,a.ts 的文本编辑器就是活动的那个
fake.executeCommand.mockImplementation(async (command: string, editorId: string) => {
if (command === 'reopenActiveEditorWith' && editorId === 'default') {
fake.window.activeTextEditor = editor
fake.window.visibleTextEditors = [editor]
}
})
})
describe('编辑器右上角「预览 / 编辑」切换', () => {
it('预览切到文本编辑器再切回来:照切走前的从哪儿打开,滚到文本编辑器顶部那一行', async () => {
const opened = previews('uncommitted')
const switcher = new EditorSwitch(opened as never)
await switcher.showSource([uri, { groupId: 1 }])
await switcher.showPreview([uri, { groupId: 1 }])
expect(opened.open).toHaveBeenLastCalledWith(uri, { from: 'uncommitted', reveal: { line: 10 } }, expect.any(Function))
switcher.dispose()
})
it('文本编辑器不是从预览切过来的:切到预览不带从哪儿打开(新开的预览不比)', async () => {
const opened = previews('uncommitted')
const switcher = new EditorSwitch(opened as never)
fake.window.activeTextEditor = editor
fake.window.visibleTextEditors = [editor]
await switcher.showPreview([uri, { groupId: 1 }])
expect(opened.open).toHaveBeenLastCalledWith(uri, { reveal: { line: 10 } }, expect.any(Function))
switcher.dispose()
})
it('切过去的那个文本编辑器标签页关了就忘掉;别的编辑器组里关的不算', async () => {
const opened = previews('uncommitted')
const switcher = new EditorSwitch(opened as never)
await switcher.showSource([uri, { groupId: 1 }])
fake.tabListeners.forEach(listener => listener({ closed: [{ input: new fake.TabInputText(uri), group: { viewColumn: 2 } }] }))
await switcher.showPreview([uri, { groupId: 1 }])
expect(opened.open).toHaveBeenLastCalledWith(uri, { from: 'uncommitted', reveal: { line: 10 } }, expect.any(Function))
fake.tabListeners.forEach(listener => listener({ closed: [{ input: new fake.TabInputText(uri), group: { viewColumn: 1 } }] }))
await switcher.showPreview([uri, { groupId: 1 }])
expect(opened.open).toHaveBeenLastCalledWith(uri, { reveal: { line: 10 } }, expect.any(Function))
switcher.dispose()
})
})
+52 -15
View File
@@ -1,6 +1,7 @@
import type { Disposable, TextEditor } from 'vscode'
import type { Disposable, TextEditor, ViewColumn } from 'vscode'
import type { OpenedFrom } from '../diff/diffBase'
import type { PreviewEditorProvider } from './previewEditor'
import { commands, Range, TabInputCustom, TextEditorRevealType, Uri, window } from 'vscode'
import { commands, Range, TabInputCustom, TabInputText, TextEditorRevealType, Uri, window } from 'vscode'
import { PREVIEW_VIEW_TYPE } from './previewEditor'
/** VS Code 自带普通文本编辑器的 editor id。 */
@@ -51,27 +52,63 @@ function waitForTextEditor(uri: Uri): Promise<TextEditor | undefined> {
}).finally(() => disposables.forEach(d => d.dispose()))
}
/** 文本编辑器 → 预览(同一标签页原位替换,算从别处打开的):新预览先滚到文本编辑器顶部那一行所在的块。 */
export async function showPreview(previews: PreviewEditorProvider, args: unknown[]): Promise<void> {
const uri = resourceArg(args) ?? window.activeTextEditor?.document.uri
const topLine = uri ? textEditorOf(uri)?.visibleRanges[0]?.start.line : undefined
const reopen = (): Thenable<unknown> => commands.executeCommand(REOPEN_WITH, PREVIEW_VIEW_TYPE, ...args)
if (uri)
await previews.open(uri, { from: 'other', ...(topLine !== undefined && { reveal: { line: topLine } }) }, reopen)
else
await reopen()
/** 一个编辑器组里的一个文件:同一个标签页在预览和文本编辑器之间换着看时按它认。 */
function tabKey(viewColumn: ViewColumn | undefined, uri: Uri): string {
return `${viewColumn ?? ''}:${uri.toString()}`
}
/** 预览 → 文本编辑器(同一标签页原位替换):把预览视口顶部那块的起始行放到文本编辑器顶部。 */
export async function showSource(previews: PreviewEditorProvider, args: unknown[]): Promise<void> {
/**
* 编辑器右上角的「预览 / 编辑」:同一个标签页原位换成另一种编辑器,换回预览时照切走前的样子对比,不算从别处重新打开。
* 切走的预览是从哪儿打开的按编辑器组和文件记着,换上的那个文本编辑器标签页关了就忘掉。
*/
export class EditorSwitch implements Disposable {
/** 切到文本编辑器的预览原来是从哪儿打开的,按 [tabKey] 记 */
private readonly sourceFrom = new Map<string, OpenedFrom>()
private readonly closing: Disposable
constructor(private readonly previews: Pick<PreviewEditorProvider, 'open' | 'visibleLine' | 'openedFrom'>) {
this.closing = window.tabGroups.onDidChangeTabs(({ closed }) => {
for (const tab of closed) {
if (tab.input instanceof TabInputText)
this.sourceFrom.delete(tabKey(tab.group.viewColumn, tab.input.uri))
}
})
}
/** 文本编辑器 → 预览:新预览先滚到文本编辑器顶部那一行所在的块;这个文本编辑器是从预览切过来的,照原来的从哪儿打开。 */
async showPreview(args: unknown[]): Promise<void> {
const uri = resourceArg(args) ?? window.activeTextEditor?.document.uri
const reopen = (): Thenable<unknown> => commands.executeCommand(REOPEN_WITH, PREVIEW_VIEW_TYPE, ...args)
if (!uri) {
await reopen()
return
}
const editor = textEditorOf(uri)
const topLine = editor?.visibleRanges[0]?.start.line
const from = editor && this.sourceFrom.get(tabKey(editor.viewColumn, uri))
await this.previews.open(uri, { ...(from && { from }), ...(topLine !== undefined && { reveal: { line: topLine } }) }, reopen)
}
/** 预览 → 文本编辑器:把预览视口顶部那块的起始行放到文本编辑器顶部,记下这个预览是从哪儿打开的。 */
async showSource(args: unknown[]): Promise<void> {
const uri = resourceArg(args) ?? activeCustomEditorUri()
const line = uri ? previews.visibleLine(uri) : undefined
const line = uri ? this.previews.visibleLine(uri) : undefined
const from = uri ? this.previews.openedFrom(uri) : undefined
await commands.executeCommand(REOPEN_WITH, TEXT_EDITOR, ...args)
if (!uri || line === undefined)
if (!uri || (line === undefined && from === undefined))
return
const editor = await waitForTextEditor(uri)
if (!editor)
return
if (from)
this.sourceFrom.set(tabKey(editor.viewColumn, uri), from)
if (line === undefined)
return
const target = Math.min(line, editor.document.lineCount - 1)
editor.revealRange(new Range(target, 0, target, 0), TextEditorRevealType.AtTop)
}
dispose(): void {
this.closing.dispose()
}
}
+7 -5
View File
@@ -16,7 +16,7 @@ import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigat
import { languageOf, openingFor } from './editor/openTarget'
import { PATH_PREVIEW_SCHEME } from './editor/pathPreview'
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './editor/previewEditor'
import { showPreview, showSource } from './editor/switchEditor'
import { EditorSwitch } from './editor/switchEditor'
import { WorkspaceEntries } from './entries/workspaceEntries'
import { registerCopyCommands } from './explorer/copyCommands'
import { FileTreeProvider } from './explorer/fileTree'
@@ -135,9 +135,10 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
uri => locator.reveal(uri),
(subject, request) => features.ask(subject, request),
)
const switcher = new EditorSwitch(previews)
const openChat = async (page: Uri | undefined): Promise<void> => {
if (page)
await previews.open(page, { from: 'other', focusPath: true })
await previews.open(page, { focusPath: true })
}
context.subscriptions.push(
@@ -153,7 +154,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
const languageId = await languageOf(uri)
return languageId !== undefined && registry.languageFor(languageId) !== undefined
},
uri => previews.open(uri, { from: 'other', focusPath: true }),
uri => previews.open(uri, { focusPath: true }),
),
workspace.registerTextDocumentContentProvider(PATH_PREVIEW_SCHEME, {
provideTextDocumentContent: () => '',
@@ -171,6 +172,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
favorites,
features,
heads,
switcher,
// 主栏审查时刷新是重读 PR 改过的文件,平时是重读工作区文件树
commands.registerCommand('superpowers.codeReader.refresh', () => review.reviewing ? review.reload() : files.refresh()),
commands.registerCommand('superpowers.codeReader.pickReviewIssue', () => review.pick()),
@@ -201,8 +203,8 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
await openFile(found.uri, 'other', registry, previews, found.spot)
}),
// 标题栏按钮传来的是 (文件 Uri, { groupId }),原样转给 VS Code 定位是哪一组的哪个标签
commands.registerCommand('superpowers.codeReader.showPreview', (...args: unknown[]) => showPreview(previews, args)),
commands.registerCommand('superpowers.codeReader.showSource', (...args: unknown[]) => showSource(previews, args)),
commands.registerCommand('superpowers.codeReader.showPreview', (...args: unknown[]) => switcher.showPreview(args)),
commands.registerCommand('superpowers.codeReader.showSource', (...args: unknown[]) => switcher.showSource(args)),
commands.registerCommand(NEXT_OCCURRENCE, () => previews.moveOccurrence(1)),
commands.registerCommand(PREVIOUS_OCCURRENCE, () => previews.moveOccurrence(-1)),
)
+1 -1
View File
@@ -468,7 +468,7 @@ export type OpenedFrom = 'uncommitted' | 'other'
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了
* openedFrom:这个标签页是从哪儿打开的,记进页面状态;ready 后来一次,开着时又被点开再来
* openedFrom:这个标签页是从哪儿打开的,记进页面状态;ready 后来一次,开着时又从侧栏点开、换了从哪儿打开的再来
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了