Files
superwork/vscode/src/codeReader/index.ts
T

153 lines
8.4 KiB
TypeScript

import type { ExtensionContext, Uri } from 'vscode'
import type { CodeReaderApi, RuleLocation, RuleReference } from './contract'
import type { FileNode } from './explorer/fileTree'
import type { IndexedName } from './nameResolution'
import { commands, window, workspace } from 'vscode'
import { logger } from '../logging/logger'
import { packageCommandRules } from './commands/packageCommands'
import { shellCommandRules } from './commands/shellCommands'
import { systemCommandRules } from './commands/systemCommands'
import { BlockFilter } from './editor/blockFilter'
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './editor/previewEditor'
import { showPreview, showSource } from './editor/switchEditor'
import { registerCopyCommands } from './explorer/copyCommands'
import { FileTreeProvider } from './explorer/fileTree'
import { TreeFilters } from './explorer/treeFilters'
import { createBashAdapter } from './languages/bash'
import { createDockerfileAdapter } from './languages/dockerfile'
import { createPythonAdapter } from './languages/python'
import { createTypeScriptAdapter } from './languages/typescript'
import { resolveReference } from './libraryRules'
import { indexedNames } from './nameResolution'
import { registerNoteCommands } from './notes/commands'
import { FileNotes } from './notes/fileNotes'
import { CodeQa } from './qa/codeQa'
import { CodeReaderRegistry } from './registry'
import { ReviewMode } from './review/reviewMode'
import { celeryRule } from './rules/celery'
import { dataclassRule } from './rules/dataclass'
import { enumRule } from './rules/enum'
import { fastapiRule } from './rules/fastapi'
import { pydanticRule } from './rules/pydantic'
import { reactRule } from './rules/react'
import { sqlalchemyRule } from './rules/sqlalchemy'
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
import { bashZhTranslator } from './translators/bashZh'
import { dockerfileZhTranslator } from './translators/dockerfileZh'
import { pythonZhTranslator } from './translators/pythonZh'
import { typescriptZhTranslator } from './translators/typescriptZh'
import { UncommittedMode } from './uncommitted/uncommittedMode'
import { workspaceIndexHost } from './workspaceFiles'
import { WorkspaceIndexes } from './workspaceIndex'
/** 当前语言插件覆盖的 languageId,给 editor/title 的 when 子句判断要不要显示「预览」。 */
const LANGUAGE_IDS_CONTEXT = 'superpowers.codeReader.languageIds'
const FILES_VIEW = 'superpowers.codeReader.files'
const OPEN_COMMAND = 'superpowers.codeReader.open'
/** 按 VS Code 的判断(含 files.associations)取文件的 languageId;图片等二进制文件打不开成文本,返回 undefined。 */
async function languageOf(uri: Uri): Promise<string | undefined> {
try {
return (await workspace.openTextDocument(uri)).languageId
}
catch {
return undefined
}
}
/** 侧栏点文件:语言受支持就用预览打开,否则用普通编辑器。 */
async function openFile(uri: Uri, registry: CodeReaderRegistry): Promise<void> {
const languageId = await languageOf(uri)
if (languageId !== undefined && registry.languageFor(languageId))
// 和 vscode.open 一样按「预览标签」打开(用户关了 workbench.editor.enablePreview 时 VS Code 自会固定)
await commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, { preview: true })
else
await commands.executeCommand('vscode.open', uri)
}
/**
* 接入代码阅读:注册表、侧栏文件树(复制、文件注释、筛选:按审查工单只看 PR 改过的文件、只看未提交的更改)、预览编辑器(含问 AI)、编辑器右上角的「预览 / 编辑」切换。
* 内置的语言插件、翻译插件、库规则和命令规则与其他扩展一样通过返回的 API 注册。
*/
export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
const registry = new CodeReaderRegistry(context.globalState)
const runtime = new TreeSitterRuntime(bundledWasmDir())
const report = (message: string): void => logger.add({ level: 'warn', source: 'codeReader', message })
const indexes = new WorkspaceIndexes(workspaceIndexHost(async (languageId, source) => {
const adapter = registry.languageFor(languageId)
if (!adapter)
throw new Error(`没有「${languageId}」的语言插件`)
return runtime.parse(adapter, languageId, source)
}, report))
const resolve = (reference: RuleReference): Promise<RuleLocation[]> => resolveReference(reference, registry.libraryRules(), indexes, report)
const qa = new CodeQa(context.globalState)
const namesIn = async (languageId: string, name: string): Promise<IndexedName[]> => {
const adapter = registry.languageFor(languageId)
return adapter ? indexedNames((index, key) => indexes.find(index, key), adapter, name) : []
}
const notes = new FileNotes()
const files = new FileTreeProvider(OPEN_COMMAND, notes)
const view = window.createTreeView(FILES_VIEW, { treeDataProvider: files, showCollapseAll: true })
const filters = new TreeFilters(files, view)
const review = new ReviewMode(context, FILES_VIEW, view, files, filters)
const uncommitted = new UncommittedMode(context, FILES_VIEW, view, files, filters)
// 侧栏开着的筛选定预览对比什么:未提交的更改对比上一次提交,审查工单对比 PR 的目标分支
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState), resolve, qa, namesIn, (index, key) => indexes.find(index, key), filters)
context.subscriptions.push(
qa,
notes,
indexes,
registerNoteCommands(notes),
registerCopyCommands(),
registry.onDidChange(() => {
void commands.executeCommand('setContext', LANGUAGE_IDS_CONTEXT, registry.languageIds())
previews.refresh()
}),
window.registerCustomEditorProvider(PREVIEW_VIEW_TYPE, previews),
files,
view,
filters,
review,
uncommitted,
// 开着筛选时刷新是重读它摆的文件(PR 改过的、git 状态),平时是重读工作区文件树
commands.registerCommand('superpowers.codeReader.refresh', () => filters.active ? filters.active.reload() : files.refresh()),
commands.registerCommand('superpowers.codeReader.pickReviewIssue', () => review.pick()),
commands.registerCommand('superpowers.codeReader.showUncommitted', () => uncommitted.enter()),
commands.registerCommand('superpowers.codeReader.hideTests', () => uncommitted.setTestsHidden(true)),
commands.registerCommand('superpowers.codeReader.showTests', () => uncommitted.setTestsHidden(false)),
// 「更改」组文件行上的按钮传来的是这一行的节点
commands.registerCommand('superpowers.codeReader.stage', (node?: FileNode) => uncommitted.stage(node)),
commands.registerCommand('superpowers.codeReader.showAllFiles', () => filters.active?.exit()),
commands.registerCommand(OPEN_COMMAND, (uri: Uri) => openFile(uri, registry)),
// 标题栏按钮传来的是 (文件 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(NEXT_OCCURRENCE, () => previews.moveOccurrence(1)),
commands.registerCommand(PREVIOUS_OCCURRENCE, () => previews.moveOccurrence(-1)),
)
const api: CodeReaderApi = registry
const wasmDir = bundledWasmDir()
context.subscriptions.push(
api.registerLanguage(createTypeScriptAdapter(wasmDir)),
api.registerLanguage(createPythonAdapter(wasmDir)),
api.registerLanguage(createBashAdapter(wasmDir)),
api.registerLanguage(createDockerfileAdapter(wasmDir)),
api.registerTranslator(typescriptZhTranslator),
api.registerTranslator(pythonZhTranslator),
api.registerTranslator(bashZhTranslator),
api.registerTranslator(dockerfileZhTranslator),
...[...shellCommandRules, ...systemCommandRules, ...packageCommandRules].map(rule => api.registerCommandRule(rule)),
api.registerLibraryRule(sqlalchemyRule),
api.registerLibraryRule(celeryRule),
api.registerLibraryRule(enumRule),
api.registerLibraryRule(dataclassRule),
api.registerLibraryRule(pydanticRule),
api.registerLibraryRule(fastapiRule),
api.registerLibraryRule(reactRule),
)
return api
}