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

247 lines
13 KiB
TypeScript

import type { ExtensionContext, Uri } from 'vscode'
import type { CodeReaderApi, RuleLocation, RuleReference } from './contract'
import type { OpenedFrom } from './diff/diffBase'
import type { TargetSpot } from './editor/openTarget'
import type { FileNode } from './explorer/fileTree'
import type { FeatureRow } from './features/featureViews'
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 { previewDiffBases } from './diff/diffBase'
import { BlockFilter } from './editor/blockFilter'
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
import { languageOf, openingFor } from './editor/openTarget'
import { PATH_PREVIEW_SCHEME } from './editor/pathPreview'
import { PreviewEditorProvider } from './editor/previewEditor'
import { EditorSwitch } from './editor/switchEditor'
import { WorkspaceEntries } from './entries/workspaceEntries'
import { registerCopyCommands } from './explorer/copyCommands'
import { FileTreeProvider } from './explorer/fileTree'
import { registerPathAsk } from './explorer/pathAskPanel'
import { registerRevealCommands } from './explorer/revealCommands'
import { RowDecorator } from './explorer/rowDecorator'
import { TreeLocator } from './explorer/treeLocator'
import { favoriteCommand } from './favorites/favoriteSlots'
import { FavoriteViews } from './favorites/favoriteViews'
import { featureCommand } from './features/features'
import { FeatureViews, locateItem, OPEN_FEATURE_ITEM } from './features/featureViews'
import { createBashAdapter } from './languages/bash'
import { createDockerfileAdapter } from './languages/dockerfile'
import { createPythonAdapter } from './languages/python'
import { createRustAdapter } from './languages/rust'
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 { axumRule } from './rules/axum'
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 { rustImplRule } from './rules/rustImpl'
import { serdeRule } from './rules/serde'
import { sqlalchemyRule } from './rules/sqlalchemy'
import { sqlxRule } from './rules/sqlx'
import { tauriRule } from './rules/tauri'
import { thiserrorRule } from './rules/thiserror'
import { bundledWasmDir, indexGrammar, TreeSitterRuntime } from './runtime'
import { SlotPool } from './slots/slotPool'
import { SLOTS } from './slots/slots'
import { readCodeColors, ThemeColors } from './theme/codeColors'
import { bashZhTranslator } from './translators/bashZh'
import { dockerfileZhTranslator } from './translators/dockerfileZh'
import { pythonZhTranslator } from './translators/pythonZh'
import { rustZhTranslator } from './translators/rustZh'
import { typescriptZhTranslator } from './translators/typescriptZh'
import { HeadBases } from './uncommitted/headBases'
import { UncommittedView } from './uncommitted/uncommittedView'
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'
/**
* 侧栏点文件、点「了解一块代码」栏里的一条:语言受支持就用预览打开、告诉它是从哪一栏点开的(`from`),否则用 VS Code 默认的打开方式(见 [openingFor])。
* 给了 `spot`(条目的符号在哪)就跳过去:预览滚到那一块、闪一下,普通编辑器选中那个名字。
*/
async function openFile(uri: Uri, from: OpenedFrom, registry: CodeReaderRegistry, previews: PreviewEditorProvider, spot?: TargetSpot): Promise<void> {
const opening = openingFor(await languageOf(uri), languageId => registry.languageFor(languageId) !== undefined, spot)
if (opening.kind === 'default') {
await commands.executeCommand('vscode.open', uri, opening.selection && { selection: opening.selection })
return
}
await previews.open(uri, { from, ...(opening.reveal && { reveal: opening.reveal }) })
}
/**
* 接入代码阅读:注册表、侧栏的几栏(主栏的文件树可以按审查工单只看 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 })
// 有语言插件的按它的语法包解析,没有的(接口契约 openapi.json)用索引专用的语法包
const indexes = new WorkspaceIndexes(workspaceIndexHost(async (languageId, source) => {
const grammar = registry.languageFor(languageId) ?? indexGrammar(bundledWasmDir(), languageId)
if (!grammar)
throw new Error(`没有「${languageId}」的语言插件`)
return runtime.parse(grammar, languageId, source)
}, report))
const resolve = (reference: RuleReference): Promise<RuleLocation[]> => resolveReference(reference, registry.libraryRules(), indexes, report)
const qa = new CodeQa(context, context.workspaceState)
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 entries = new WorkspaceEntries()
const decorator = new RowDecorator(notes, entries)
const files = new FileTreeProvider({ command: OPEN_COMMAND, from: 'other' }, decorator)
const view = window.createTreeView(FILES_VIEW, { treeDataProvider: files, showCollapseAll: true })
const locator = new TreeLocator(files, view)
const review = new ReviewMode(context, FILES_VIEW, view, files)
const uncommitted = new UncommittedView(context, OPEN_COMMAND, decorator)
// 收藏目录、「了解一块代码」共用一池栏位
const slots = new SlotPool()
const favorites = new FavoriteViews(context.workspaceState, slots, OPEN_COMMAND, decorator)
const features = new FeatureViews(context.workspaceState, slots, qa, item => locateItem(item, { adapterFor: languageId => registry.languageFor(languageId), runtime }))
const heads = new HeadBases()
// 预览对比什么:审查工作副本里的对比 PR 的目标分支,从「未提交的更改」一栏点开的有改动的文件对比上一次提交,别处打开的不比
const diffBases = previewDiffBases(review, heads)
// 预览里代码的上色、卡片的种类色跟着当前颜色主题走
const codeColors = new ThemeColors(readCodeColors)
const previews = new PreviewEditorProvider(
context.extensionUri,
registry,
runtime,
new BlockFilter(context.workspaceState),
codeColors,
resolve,
qa,
namesIn,
(index, key) => indexes.find(index, key),
diffBases,
entries,
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, { focusPath: true })
}
context.subscriptions.push(
qa,
codeColors,
notes,
entries,
indexes,
registerNoteCommands(notes),
registerCopyCommands(),
registerRevealCommands(),
registerPathAsk(
async (uri) => {
const languageId = await languageOf(uri)
return languageId !== undefined && registry.languageFor(languageId) !== undefined
},
uri => previews.open(uri, { focusPath: true }),
),
workspace.registerTextDocumentContentProvider(PATH_PREVIEW_SCHEME, {
provideTextDocumentContent: () => '',
}),
registry.onDidChange(() => {
void commands.executeCommand('setContext', LANGUAGE_IDS_CONTEXT, registry.languageIds())
previews.refresh()
}),
previews.register(),
files,
view,
locator,
review,
uncommitted,
favorites,
features,
heads,
switcher,
// 主栏审查时刷新是重读 PR 改过的文件,平时是重读工作区文件树
commands.registerCommand('superpowers.codeReader.refresh', () => review.reviewing ? review.reload() : files.refresh()),
commands.registerCommand('superpowers.codeReader.pickReviewIssue', () => review.pick()),
commands.registerCommand('superpowers.codeReader.hideReviewTests', () => review.setTestsHidden(true)),
commands.registerCommand('superpowers.codeReader.showReviewTests', () => review.setTestsHidden(false)),
commands.registerCommand('superpowers.codeReader.showAllFiles', () => review.exit()),
commands.registerCommand('superpowers.codeReader.refreshUncommitted', () => uncommitted.reload()),
commands.registerCommand('superpowers.codeReader.hideUncommittedTests', () => uncommitted.setTestsHidden(true)),
commands.registerCommand('superpowers.codeReader.showUncommittedTests', () => uncommitted.setTestsHidden(false)),
// 「更改」组文件行上的按钮传来的是这一行的节点
commands.registerCommand('superpowers.codeReader.stage', (node?: FileNode) => uncommitted.stage(node)),
// 目录行上的按钮传来的是这一行的节点;栏标题栏上的按钮不带是哪一栏,所以每个栏位各一套命令
commands.registerCommand('superpowers.codeReader.addFavorite', (node?: FileNode) => favorites.add(node)),
commands.registerCommand('superpowers.codeReader.findFeature', async () => openChat(await features.find())),
...SLOTS.flatMap(slot => [
commands.registerCommand(favoriteCommand('rename', slot), () => favorites.rename(slot)),
commands.registerCommand(favoriteCommand('remove', slot), () => favorites.remove(slot)),
commands.registerCommand(featureCommand('openChat', slot), () => openChat(features.chatPage(slot))),
commands.registerCommand(featureCommand('rename', slot), () => features.rename(slot)),
commands.registerCommand(featureCommand('remove', slot), () => features.remove(slot)),
]),
// 侧栏各栏的文件行传来的是 (文件 Uri, 从哪一栏点开的)
commands.registerCommand(OPEN_COMMAND, (uri: Uri, from: OpenedFrom) => openFile(uri, from, registry, previews)),
// 「了解一块代码」栏里的条目行传来的是这一行:按眼下的代码找到它再打开
commands.registerCommand(OPEN_FEATURE_ITEM, async (row?: FeatureRow) => {
const found = await features.target(row)
if (found)
await openFile(found.uri, 'other', registry, previews, found.spot)
}),
// 标题栏按钮传来的是 (文件 Uri, { groupId }),原样转给 VS Code 定位是哪一组的哪个标签
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)),
)
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.registerLanguage(createRustAdapter(wasmDir)),
api.registerTranslator(typescriptZhTranslator),
api.registerTranslator(pythonZhTranslator),
api.registerTranslator(bashZhTranslator),
api.registerTranslator(dockerfileZhTranslator),
api.registerTranslator(rustZhTranslator),
...[...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),
api.registerLibraryRule(rustImplRule),
api.registerLibraryRule(serdeRule),
api.registerLibraryRule(thiserrorRule),
api.registerLibraryRule(axumRule),
api.registerLibraryRule(sqlxRule),
api.registerLibraryRule(tauriRule),
)
return api
}