Files
superwork/vscode/src/codeReader/editor/switchEditor.ts
T

115 lines
5.1 KiB
TypeScript

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<TextEditor | undefined> {
const found = textEditorOf(uri)
if (found)
return Promise.resolve(found)
const disposables: Disposable[] = []
return new Promise<TextEditor | undefined>((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<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 ? 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()
}
}