✨ feat(vscode): 代码阅读入口、文件树与预览编辑器

This commit is contained in:
cruldra committed 2026-09-24 21:36:00 +08:00
1 parent 4d34b8c715
commit 2b823a32ca
24 files changed
+768 -72

No files matched your search

@@ -0,0 +1,127 @@
import type { CustomTextEditorProvider, Disposable, TextDocument, Uri, Webview, WebviewPanel } from 'vscode'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
import type { ExtensionToWebview, WebviewToExtension } from './messages'
import { Range, window, workspace, WorkspaceEdit } from 'vscode'
import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { blockReplacement } from '../editText'
import { documentMessage } from './documentMessage'
export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
/** 连续输入时合并成一次重新解析。 */
const REFRESH_DELAY_MS = 150
/**
* 「代码阅读预览」自定义编辑器。预览和普通编辑器共用同一个 TextDocument:
* 块内修改走 WorkspaceEdit,未保存标记、⌘S、⌘Z 都是 VS Code 原生行为。
*/
export class PreviewEditorProvider implements CustomTextEditorProvider {
private readonly previews = new Set<Preview>()
constructor(
private readonly extensionUri: Uri,
private readonly registry: CodeReaderRegistry,
private readonly runtime: TreeSitterRuntime,
) {}
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
panel.webview.options = { enableScripts: true, localResourceRoots: [webviewUiRoot(this.extensionUri)] }
panel.webview.html = webviewHtml(panel.webview, this.extensionUri, 'code-reader.html')
const preview = new Preview(document, panel.webview, this.registry, this.runtime, languageId => this.refresh(languageId))
this.previews.add(preview)
panel.onDidDispose(() => {
this.previews.delete(preview)
preview.dispose()
})
}
/** 让已打开的预览按当前注册表重发;给了 languageId 就只刷这种语言的。 */
refresh(languageId?: string): void {
for (const preview of this.previews) {
if (languageId === undefined || preview.languageId === languageId)
void preview.push()
}
}
}
class Preview {
/** webview 发过 ready 之后才发文档,避免脚本还没起来时消息丢掉 */
private ready = false
/** 解析是异步的:只发最后一次请求的结果,慢的旧结果不能盖掉新的 */
private latest = 0
private timer: NodeJS.Timeout | undefined
private readonly disposables: Disposable[]
constructor(
private readonly document: TextDocument,
private readonly webview: Webview,
private readonly registry: CodeReaderRegistry,
private readonly runtime: TreeSitterRuntime,
private readonly onTranslatorSelected: (languageId: string) => void,
) {
this.disposables = [
webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
workspace.onDidChangeTextDocument((e) => {
if (e.document === document && e.contentChanges.length > 0)
this.schedulePush()
}),
]
}
get languageId(): string {
return this.document.languageId
}
async push(): Promise<void> {
clearTimeout(this.timer)
if (!this.ready)
return
const request = ++this.latest
const message: ExtensionToWebview = await documentMessage(this.document, this.registry, this.runtime)
if (request === this.latest)
void this.webview.postMessage(message)
}
dispose(): void {
clearTimeout(this.timer)
for (const disposable of this.disposables)
disposable.dispose()
}
private schedulePush(): void {
clearTimeout(this.timer)
this.timer = setTimeout(() => void this.push(), REFRESH_DELAY_MS)
}
private async handleMessage(msg: WebviewToExtension): Promise<void> {
switch (msg.type) {
case 'ready':
this.ready = true
await this.push()
return
case 'edit':
await this.applyEdit(msg.startLine, msg.endLine, msg.text)
return
case 'selectTranslator':
await this.registry.selectTranslator(this.languageId, msg.id)
this.onTranslatorSelected(this.languageId)
}
}
private async applyEdit(startLine: number, endLine: number, text: string): Promise<void> {
const { document } = this
const replacement = blockReplacement(line => document.lineAt(line).text, document.lineCount, startLine, endLine, text)
if (!replacement) {
logger.add({ level: 'warn', source: 'codeReader', message: `块内修改的行号 ${startLine}-${endLine} 已不在 ${document.uri.fsPath} 里,没有写回` })
return
}
if (replacement.unchanged)
return
const edit = new WorkspaceEdit()
edit.replace(document.uri, new Range(replacement.startLine, 0, replacement.endLine, replacement.endCharacter), replacement.text)
if (!await workspace.applyEdit(edit))
void window.showErrorMessage(`代码阅读:没能把修改写回 ${workspace.asRelativePath(document.uri)}`)
}
}