import type { Disposable, TextEditor, ViewColumn } from 'vscode' import type { OpenedFrom } from '../diff/diffBase' import type { PreviewEditorProvider } from './previewEditor' import { commands, Range, TabInputCustom, TabInputText, TextEditorRevealType, Uri, window } from 'vscode' import { PREVIEW_VIEW_TYPE } from './previewEditor' /** VS Code 自带普通文本编辑器的 editor id。 */ const TEXT_EDITOR = 'default' /** * VS Code 自带的「重新打开编辑器的方式」:在原标签页位置换成另一种编辑器,新编辑器打开后才返回。 * 原文件有未保存改动也不弹保存框——改动在 TextDocument 里,不跟着标签页走。 */ const REOPEN_WITH = 'reopenActiveEditorWith' /** 切换命令返回时文本编辑器还没同步到扩展这边的话,最多再等这么久 */ const EDITOR_WAIT_MS = 1000 function sameUri(a: Uri, b: Uri): boolean { return a.toString() === b.toString() } /** 标题栏按钮传来的是 (文件 Uri, { groupId }),命令面板里调用时没有参数。 */ function resourceArg(args: unknown[]): Uri | undefined { return args[0] instanceof Uri ? args[0] : undefined } function activeCustomEditorUri(): Uri | undefined { const input = window.tabGroups.activeTabGroup.activeTab?.input return input instanceof TabInputCustom ? input.uri : undefined } function textEditorOf(uri: Uri): TextEditor | undefined { const active = window.activeTextEditor if (active && sameUri(active.document.uri, uri)) return active return window.visibleTextEditors.find(editor => sameUri(editor.document.uri, uri)) } /** 编辑器变化和命令结果是分开通知到扩展这边的:没看到 uri 的文本编辑器就等它出现。 */ function waitForTextEditor(uri: Uri): Promise { const found = textEditorOf(uri) if (found) return Promise.resolve(found) const disposables: Disposable[] = [] return new Promise((resolve) => { disposables.push(window.onDidChangeVisibleTextEditors(() => { const editor = textEditorOf(uri) if (editor) resolve(editor) })) const timer = setTimeout(resolve, EDITOR_WAIT_MS, undefined) disposables.push({ dispose: () => clearTimeout(timer) }) }).finally(() => disposables.forEach(d => d.dispose())) } /** 一个编辑器组里的一个文件:同一个标签页在预览和文本编辑器之间换着看时按它认。 */ function tabKey(viewColumn: ViewColumn | undefined, uri: Uri): string { return `${viewColumn ?? ''}:${uri.toString()}` } /** * 编辑器右上角的「预览 / 编辑」:同一个标签页原位换成另一种编辑器,换回预览时照切走前的样子对比,不算从别处重新打开。 * 切走的预览是从哪儿打开的按编辑器组和文件记着,换上的那个文本编辑器标签页关了就忘掉。 */ export class EditorSwitch implements Disposable { /** 切到文本编辑器的预览原来是从哪儿打开的,按 [tabKey] 记 */ private readonly sourceFrom = new Map() private readonly closing: Disposable constructor(private readonly previews: Pick) { 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 { const uri = resourceArg(args) ?? window.activeTextEditor?.document.uri const reopen = (): Thenable => 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 { const uri = resourceArg(args) ?? activeCustomEditorUri() 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 && 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() } }