✨ feat(vscode): 代码阅读契约加命令规则(按命令名注册、读一条命令)、按位置的参数 / 输出 / 返回、解释器和选项块、分情况 / 命令 / 管道种类、常量色;模板引擎加节点读法;没有定义、高亮提供者的语言用语言插件认的同一文件里的名字跳转、高亮

This commit is contained in:
cruldra committed 2026-09-28 00:44:46 +08:00
1 parent cf09c24730
commit ff2a44f080
17 files changed
+555 -60

No files matched your search

+1 -1
View File
@@ -260,7 +260,7 @@ describe('blockView', () => {
it('跨几行写的调用:一行译文下面带摊开实参的表;calleeKind 交给翻译插件分新建还是调用', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source, undefined, call => call.callee.text === 'Detail' ? 'class' : undefined)
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { calleeKind: call => call.callee.text === 'Detail' ? 'class' : undefined })
const returned = list[0].children[0]
expect(translationText(returned)).toBe('返回 新建 Detail · 1 个参数')
expect('table' in returned && returned.table).toMatchObject({
+20 -9
View File
@@ -1,4 +1,4 @@
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
import type { TreeSitterRuntime } from './runtime'
import { blockAttributes } from './attributes'
import { commonIndent, splitLines } from './editText'
@@ -212,31 +212,42 @@ export function toBlockViews(blocks: CodeBlock[], document: SourceDocument, tran
return blocks.map((block, i) => build(block, String(i), ''))
}
/** 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)。 */
/**
* 一份源码的视图数据,和它里面起了中文名的定义(预览找用到它们的各处要用,见 displayNames.ts)、
* 语言插件自己认得出的名字(LanguageAdapter.symbols,语言扩展查不到定义、各处出现时用)。
*/
export interface DocumentViews {
blocks: BlockView[]
displayNames: DisplayName[]
symbols: LocalSymbol[]
}
/**
* 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。
* calleeKind 交出调用的名字的定义眼下查到是哪种块(SourceDocument.calleeKind)。
* 排视图数据时宿主另外交给规则、翻译插件的:用哪些库规则(RuleRun),调用的名字眼下查到定义成哪种块(SourceDocument.calleeKind),
* 命令名的命令规则(SourceDocument.commandRule)。
*/
export interface DocumentExtras {
rules?: RuleRun
calleeKind?: SourceDocument['calleeKind']
commandRule?: SourceDocument['commandRule']
}
/** 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。 */
export async function readBlockViews(
runtime: Pick<TreeSitterRuntime, 'parse'>,
adapter: LanguageAdapter,
translator: Translator | undefined,
languageId: string,
source: string,
rules: RuleRun = NO_RULES,
calleeKind?: SourceDocument['calleeKind'],
extras: DocumentExtras = {},
): Promise<DocumentViews> {
const tree = await runtime.parse(adapter, languageId, source)
try {
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
const document: SourceDocument = { source, language: adapter, markers, ...(calleeKind && { calleeKind }) }
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), document, translator, rules)
return { blocks, displayNames: names }
const { calleeKind, commandRule } = extras
const document: SourceDocument = { source, language: adapter, markers, ...(calleeKind && { calleeKind }), ...(commandRule && { commandRule }) }
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), document, translator, extras.rules)
return { blocks, displayNames: names, symbols: adapter.symbols?.(tree.rootNode, source, languageId) ?? [] }
}
finally {
tree.delete()
+30
View File
@@ -0,0 +1,30 @@
import type { CommandRule } from './contract'
/** 规则读命令时出错:记下,当它没读(命令照抄)。 */
function guarded(rule: CommandRule, report: (message: string) => void): CommandRule {
return {
...rule,
read: (command, context) => {
try {
return rule.read(command, context)
}
catch (err) {
report(`命令规则 ${rule.id} 读「${command.name.node.text}」时出错,照抄命令:${err instanceof Error ? err.message : String(err)}`)
return undefined
}
},
}
}
/** 命令名 → 命令规则(SourceDocument.commandRule):几条规则都管同一个名字时先注册的算。 */
export function commandRuleLookup(rules: readonly CommandRule[], report: (message: string) => void): (name: string) => CommandRule | undefined {
const byName = new Map<string, CommandRule>()
for (const rule of rules) {
const safe = guarded(rule, report)
for (const name of rule.commands) {
if (!byName.has(name))
byName.set(name, safe)
}
}
return name => byName.get(name)
}
+131 -3
View File
@@ -3,9 +3,14 @@ import type { Disposable } from 'vscode'
export type { Node }
/**
* 块的种类。后面几种给脚本语言(shell):interpreter 第一行 `#!` 写的解释器,options 打开 / 关掉解释器的开关(`set -euo pipefail`),
* case 按一个值分情况(整条语句,和它的每个情况),command 执行一条命令,pipeline 几条命令用管道串起来。
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
/** 源码里一段的下标:源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
export interface SourceRange { startIndex: number, endIndex: number }
@@ -41,6 +46,50 @@ export interface FunctionFacts {
decorators: Node[]
parameters: ParameterFacts[]
returnType?: Node
/**
* 不写参数表、按位置取参数的语言(shell 的 `$1`):函数里用到的第几个参数,照位置排;
* 函数里给它起了名字(`local url="$1"`)时 name 是那个名字。按参数表取参数的语言不填,填了 parameters 就是空的。
*/
positional?: PositionalParameter[]
/** 函数打印到输出的那几句(shell 的 echo / printf,没重定向到别处):调用处拿到的就是它们打印的;没有这个概念的语言不填。 */
prints?: Node[]
/** 函数的输出被调用处拿去用了(shell 同一文件里有 `$(函数名 …)`)。 */
captured?: boolean
/**
* 函数的结果怎么来(shell 这种没有返回值、只有成不成功的语言):写了 return 的是那几句(return 语句的节点),
* 没写的是空数组,意思是最后一句成不成功。别的语言不填。
*/
returns?: Node[]
}
/** 按位置取的一个参数:第几个(1 起),函数里给它起的名字。 */
export interface PositionalParameter {
position: number
name?: Node
}
/** 第一行 `#!` 写的解释器。 */
export interface InterpreterFacts {
/** 解释器叫什么:`#!/usr/bin/env bash` 和 `#!/bin/bash` 都是 bash。 */
program: string
/** 怎么找到它:交给 env 按 PATH 找,还是写死的路径。 */
lookup: 'env' | 'path'
/** `#!` 后面写的整句(`/usr/bin/env bash`、`/bin/bash -e`)。 */
command: string
}
/** 打开 / 关掉解释器的一个开关(shell 的 `set -e`、`set -o pipefail`)。 */
export interface SwitchFacts {
/** 开关的 id:语言自己的叫法(bash 是长名字 errexit、nounset、pipefail……),翻译插件按它起名。 */
id: string
on: boolean
/** 源码里怎么写的(`-e`、`-o pipefail`)。 */
flag: string
}
export interface OptionsFacts {
/** 照源码顺序。 */
switches: SwitchFacts[]
}
/**
@@ -205,6 +254,68 @@ export interface CallFacts {
arguments: CallArgument[]
}
/**
* 命令的一个重定向:把输出(1)、错误输出(2)或两样都写到 / 追加到一个文件,并进另一路,或从文件读输入。
* `>&2` 是输出并进错误输出,`2>/dev/null` 是错误输出写到 /dev/null。
*/
export type Redirect
= | { kind: 'write' | 'append', stream: 'output' | 'error' | 'both', target: Node }
| { kind: 'merge', stream: 'output' | 'error', into: 'output' | 'error' }
| { kind: 'read', source: Node }
/** 命令的一个词:节点,和它写死的文字(去掉引号、转义;里面有展开 `$x`、`$(…)` 的没有),规则认选项、选项的值用它。 */
export interface CommandWord {
node: Node
literal?: string
}
/** 执行一条命令(shell):语言插件从语法树读出结构(LanguageAdapter.command),命令规则按命令名读成译文(CommandRule)。 */
export interface CommandFacts {
/** 命令名(`rm`、`curl`、同一文件里定义的函数名)。 */
name: CommandWord
/** 命令名后面的各个词,照源码顺序:选项、选项的值、要处理的东西都在里面,规则自己认。 */
arguments: CommandWord[]
redirects: Redirect[]
}
/** 命令规则读出来的:一行译文;value 表示读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作。 */
export interface CommandReading {
text: Inline[]
value?: boolean
}
/** 命令规则读命令时能用的。 */
export interface CommandContext {
/** 当前翻译插件的 displayLanguage;规则按它挑自己的词表,没有这种语言的词表就不读(照抄源码)。 */
displayLanguage: string
/** 一个词照常翻(变量、字符串、展开读成译文,名字带源码下标,能跳到定义、高亮各处出现)。 */
translate: (word: Node) => Inline[]
/** 读另一条写在实参里的命令(`find … -exec rm -rf {} +` 里的 rm):name、args 是那条命令的词;没有它的规则时没有。 */
command: (name: CommandWord, args: CommandWord[]) => CommandReading | undefined
}
/**
* 命令规则:按命令名注册,像库规则一样把一条命令(它的选项)读成译文:`rm -rf dest` → 删除 dest(连同里面的东西,不提示)。
* 认不出的选项照抄;读不了(不认识的写法)返回 undefined,宿主照抄命令名、实参照常翻。没有规则的命令同样照抄。
*/
export interface CommandRule {
id: string // 'shell.rm'
label: string // 'rm'
languageIds: string[]
/** 管哪些命令名;几条规则都管同一个名字时先注册的算。 */
commands: string[]
read: (command: CommandFacts, context: CommandContext) => CommandReading | undefined
}
/**
* 一份源码里自己认得出的一个名字(语言插件认,不问语言扩展):定义处,和用到它的各处(含定义处)。
* VS Code 里没有这种语言的定义提供者、高亮提供者时(shell),宿主拿它跳到定义、高亮各处出现:跳到的是用到的那处之前最近的定义。
*/
export interface LocalSymbol {
definitions: SourceRange[]
occurrences: SourceRange[]
}
export interface DeclaratorFacts {
/** 变量名;解构声明是整个模式。 */
name: Node
@@ -266,7 +377,11 @@ export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
export interface ExceptionBlock extends BlockShape { kind: 'exception', facts: ExceptionFacts }
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock
/** 解释器块只有 `#!` 那一行。 */
export interface InterpreterBlock extends BlockShape { kind: 'interpreter', facts: InterpreterFacts }
/** 开关块是一整条 `set …`,每个开关一行。 */
export interface OptionsBlock extends BlockShape { kind: 'options', facts: OptionsFacts }
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
@@ -294,6 +409,7 @@ export type TokenRole
| 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null …
| 'operator' // 被翻成文字的运算符:全等 并且 或者 非
| 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 …
| 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME)
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
/**
@@ -392,6 +508,10 @@ export interface LanguageAdapter {
iteration?: (node: Node) => IterationFacts | undefined
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
call?: (node: Node) => CallFacts | undefined
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;别的节点没有,没有命令的语言不填。 */
command?: (node: Node) => CommandFacts | undefined
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[]
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
displayNames?: DisplayNameReader
}
@@ -434,6 +554,8 @@ export interface SourceDocument {
* 排视图数据是同步的,这里只交出已经查到的;还没查到、查不到、宿主不查(没给)都是 undefined,查到了宿主重排、重发。
*/
calleeKind?: (call: CallFacts) => BlockKind | undefined
/** 这个命令名的命令规则(这种语言注册的,先注册的算);没有、或宿主不给时没有,命令照抄。规则出错由宿主记下、当它没读。 */
commandRule?: (name: string) => CommandRule | undefined
}
/** 筛选属性和值的显示名,筛选栏、块的种类(图标的悬停提示)用它;没给名字的显示 id。 */
@@ -467,8 +589,9 @@ export interface Translator {
export interface ThemeColor { dark: string, light: string }
/**
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 18 个给内置种类,后面的给库规则的种类;
* 库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),没有按任意名字找图标的路。
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 22 个给内置种类(命令、管道共用一个),
* 后面的给库规则的种类;库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),
* 没有按任意名字找图标的路。
*/
export const KIND_ICON_NAMES = [
'import',
@@ -489,6 +612,10 @@ export const KIND_ICON_NAMES = [
'corner-down-left',
'code',
'message-square-text',
'terminal',
'sliders-horizontal',
'signpost',
'square-terminal',
'database',
'package',
'server-cog',
@@ -646,4 +773,5 @@ export interface CodeReaderApi {
registerLanguage: (adapter: LanguageAdapter) => Disposable
registerTranslator: (translator: Translator) => Disposable
registerLibraryRule: (rule: LibraryRule) => Disposable
registerCommandRule: (rule: CommandRule) => Disposable
}
@@ -1,9 +1,10 @@
import type { DisplayName, RuleContext, SourceDocument } from '../contract'
import type { DisplayName, LocalSymbol, RuleContext, SourceDocument } from '../contract'
import type { CodeReaderRegistry } from '../registry'
import type { TreeSitterRuntime } from '../runtime'
import type { DocumentMessage } from './messages'
import { fileAttributes, mergeAttributeNames } from '../attributes'
import { readBlockViews } from '../blockView'
import { commandRuleLookup } from '../commandRules'
import { ruleAttributeNames } from '../libraryRules'
/** 生成预览数据要读的那点文档信息(TextDocument 的子集)。 */
@@ -13,10 +14,11 @@ export interface DocumentSnapshot {
getText: () => string
}
/** 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)。 */
/** 发给预览的文档,和这份源码里起了中文名的定义(扩展端接着找用到它们的各处)、语言插件自己认得出的名字(LocalSymbol)。 */
export interface DocumentRead {
message: DocumentMessage
displayNames: DisplayName[]
symbols: LocalSymbol[]
}
/**
@@ -26,7 +28,7 @@ export interface DocumentRead {
*/
export async function documentMessage(
document: DocumentSnapshot,
registry: Pick<CodeReaderRegistry, 'languageFor' | 'translatorsFor' | 'selectedTranslator' | 'rulesFor'>,
registry: Pick<CodeReaderRegistry, 'languageFor' | 'translatorsFor' | 'selectedTranslator' | 'rulesFor' | 'commandRulesFor'>,
runtime: Pick<TreeSitterRuntime, 'parse'>,
report: (message: string) => void,
lookup?: RuleContext['lookup'],
@@ -47,14 +49,18 @@ export async function documentMessage(
}
const adapter = registry.languageFor(languageId)
if (!adapter)
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [], symbols: [] }
try {
const rules = registry.rulesFor(languageId)
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup }, calleeKind)
const { blocks, displayNames, symbols } = await readBlockViews(runtime, adapter, translator, languageId, source, {
rules: { rules, report, lookup },
calleeKind,
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
})
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames }
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames, symbols }
}
catch (err) {
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [] }
return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [] }
}
}
+35 -6
View File
@@ -1,7 +1,7 @@
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { BlockView } from '../blockView'
import type { BlockKind, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
import type { DefinitionStart, IndexedName, NameSources } from '../nameResolution'
import type { CodeQa } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
@@ -10,13 +10,14 @@ import type { TreeSitterRuntime } from '../runtime'
import type { IndexedEntry } from '../workspaceIndex'
import type { BlockFilter } from './blockFilter'
import type { Definition, FoundLocation } from './definitions'
import type { ExtensionToWebview, Reveal, WebviewToExtension } from './messages'
import type { ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
import { logger } from '../../logging/logger'
import { webviewHtml, webviewUiRoot } from '../../webview/html'
import { DocumentCallees } from '../calleeKinds'
import { blockReplacement } from '../editText'
import { locate } from '../folderStore'
import { definitionFor, symbolAt, symbolOccurrences } from '../localSymbols'
import { DocumentNames, resolveNames } from '../nameResolution'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { DocumentLookups } from '../ruleLookups'
@@ -128,6 +129,8 @@ class Preview {
visibleLine: number | undefined
/** 最近一次发出去的块树和它的源码:问 AI 时按块的记号在里面找那块代码 */
private sent: { blocks: BlockView[], source: string } | undefined
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
private symbols: { version: number, list: LocalSymbol[] } | undefined
/** 这份文档各版本显示成中文名的名字 */
private readonly names: DocumentNames
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
@@ -150,7 +153,10 @@ class Preview {
) {
this.definitions = new DefinitionSources({
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
language: offset => languageDefinitions(document.uri, document.positionAt(offset)),
language: async (offset) => {
const found = await languageDefinitions(document.uri, document.positionAt(offset))
return found.length > 0 ? found : this.localDefinitions(offset)
},
})
this.names = new DocumentNames((version, names) => {
if (this.ready && version === document.version)
@@ -228,13 +234,14 @@ class Preview {
const source = this.document.getText()
const session = this.lookups.session()
const callees = this.callees.session()
const { message, displayNames } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
const { message, displayNames, symbols } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
}, session.lookup, callees.calleeKind)
if (request !== this.latest || !this.ready)
return
this.definitions.remember(message.version, message.blocks)
this.sent = { blocks: message.blocks, source }
this.symbols = { version, list: symbols }
const reveal = this.pendingReveal
this.pendingReveal = undefined
this.post({ ...message, names: this.names.known(version), ...(reveal && { reveal }) })
@@ -260,6 +267,28 @@ class Preview {
return null
}
/** 这份文档里语言插件自己认得出、盖住 offset 的名字(只认最近一次发出去的那个版本)。 */
private localSymbol(offset: number): LocalSymbol | undefined {
return this.symbols?.version === this.document.version ? symbolAt(this.symbols.list, offset) : undefined
}
/** 语言扩展查不到定义时:语言插件自己认得出的名字的定义(见 definitionFor)。 */
private localDefinitions(offset: number): FoundLocation[] {
const symbol = this.localSymbol(offset)
const target = symbol && definitionFor(symbol, offset)
if (!target)
return []
const { document } = this
return [{ uri: document.uri, range: new Range(document.positionAt(target.startIndex), document.positionAt(target.endIndex)) }]
}
/** offset 处名字的各处出现:先问语言扩展,它答不上来时用语言插件自己认得出的名字(见 symbolOccurrences)。 */
private async occurrences(offset: number): Promise<Occurrence[]> {
const found = await occurrencesOf(this.document, offset)
const symbol = found.length > 0 ? undefined : this.localSymbol(offset)
return symbol ? symbolOccurrences(symbol) : found
}
/** 这份文档里 offset 处名字的定义(语言扩展查的)落在哪儿。 */
private async definitionStarts(offset: number): Promise<DefinitionStart[]> {
const { document } = this
@@ -272,7 +301,7 @@ class Preview {
const { document } = this
return {
uri: document.uri.toString(),
occurrences: async offset => (await occurrencesOf(document, offset)).map(({ start, end }) => ({ start, end })),
occurrences: async offset => (await this.occurrences(offset)).map(({ start, end }) => ({ start, end })),
definitions: offset => this.definitionStarts(offset),
indexed: name => this.owner.indexedNames(document.languageId, name),
}
@@ -403,7 +432,7 @@ class Preview {
const { document } = this
if (version !== document.version)
return
const occurrences = await occurrencesOf(document, offset)
const occurrences = await this.occurrences(offset)
if (version === document.version && this.ready)
this.post({ type: 'occurrencesFound', id, occurrences })
}
+1 -1
View File
@@ -206,7 +206,7 @@ function commentNodes(block: CodeBlock): Node[] {
function ownDoc(block: CodeBlock): Node | undefined {
if (!('facts' in block))
return block.doc
return block.kind === 'import' ? undefined : block.facts.doc
return block.kind === 'import' || block.kind === 'interpreter' || block.kind === 'options' ? undefined : block.facts.doc
}
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
@@ -0,0 +1,33 @@
import type { LocalSymbol, SourceRange } from './contract'
import { describe, expect, it } from 'vitest'
import { definitionFor, symbolAt, symbolOccurrences } from './localSymbols'
function range(startIndex: number, endIndex: number): SourceRange {
return { startIndex, endIndex }
}
// out=1(0-3);echo $out(10-13);out=2(15-18);echo $out(25-28)
const OUT: LocalSymbol = { definitions: [range(0, 3), range(15, 18)], occurrences: [range(0, 3), range(10, 13), range(15, 18), range(25, 28)] }
const URL: LocalSymbol = { definitions: [range(40, 43)], occurrences: [range(30, 33), range(40, 43)] }
describe('语言插件认出的同一文件里的名字', () => {
it('找盖住位置的名字;名字外面没有', () => {
expect(symbolAt([URL, OUT], 11)).toBe(OUT)
expect(symbolAt([URL, OUT], 13)).toBeUndefined()
})
it('跳到之前最近的一处定义;前面没有定义就跳到第一处', () => {
expect(definitionFor(OUT, 11)).toEqual(range(0, 3))
expect(definitionFor(OUT, 26)).toEqual(range(15, 18))
expect(definitionFor(URL, 31)).toEqual(range(40, 43))
})
it('各处出现:定义处算写,别处算读', () => {
expect(symbolOccurrences(OUT)).toEqual([
{ start: 0, end: 3, kind: 'write' },
{ start: 10, end: 13, kind: 'read' },
{ start: 15, end: 18, kind: 'write' },
{ start: 25, end: 28, kind: 'read' },
])
})
})
+25
View File
@@ -0,0 +1,25 @@
import type { LocalSymbol, SourceRange } from './contract'
/**
* 语言插件自己认得出的名字(LanguageAdapter.symbols)怎么用:VS Code 里没有这种语言的定义、高亮提供者时(shell),
* 宿主拿它跳到定义、高亮各处出现。这里不碰 VS Code。
*/
/** 盖住 offset 的那个名字。 */
export function symbolAt(symbols: readonly LocalSymbol[], offset: number): LocalSymbol | undefined {
return symbols.find(symbol => symbol.occurrences.some(range => range.startIndex <= offset && offset < range.endIndex))
}
/** offset 处用到的名字跳到哪个定义:它之前最近的一处(同名的变量前后赋过几次值),前面没有就是第一处。 */
export function definitionFor(symbol: LocalSymbol, offset: number): SourceRange | undefined {
return symbol.definitions.filter(range => range.startIndex <= offset).at(-1) ?? symbol.definitions[0]
}
/** 名字的各处出现:定义处算写,别处算读。 */
export function symbolOccurrences(symbol: LocalSymbol): { start: number, end: number, kind: 'read' | 'write' }[] {
return symbol.occurrences.map(({ startIndex, endIndex }) => ({
start: startIndex,
end: endIndex,
kind: symbol.definitions.some(range => range.startIndex === startIndex) ? 'write' : 'read',
}))
}
+1 -1
View File
@@ -25,7 +25,7 @@ const ROUTES = [
].join('\n')
function read(source: string): ReturnType<typeof readBlockViews> {
return readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { rules: [enumRule], report: () => {} })
return readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { rules: { rules: [enumRule], report: () => {} } })
}
function allProperties(blocks: readonly BlockView[]): string[] {
+14 -1
View File
@@ -1,5 +1,5 @@
import type { Memento } from 'vscode'
import type { LanguageAdapter, LibraryRule, Translator } from './contract'
import type { CommandRule, LanguageAdapter, LibraryRule, Translator } from './contract'
import { describe, expect, it } from 'vitest'
import { CodeReaderRegistry, TRANSLATOR_CHOICE_KEY } from './registry'
@@ -96,6 +96,19 @@ describe('codeReaderRegistry', () => {
expect(changes).toBe(4)
})
it('命令规则按语言过滤、保持注册顺序;注销后不再给', () => {
const registry = new CodeReaderRegistry(fakeMemento())
const command = (id: string, languageIds: string[]): CommandRule => ({ id, label: id, languageIds, commands: [id], read: () => undefined })
registry.registerCommandRule(command('rm', ['shellscript']))
const curl = registry.registerCommandRule(command('curl', ['shellscript']))
registry.registerCommandRule(command('ls', ['powershell']))
expect(registry.commandRulesFor('shellscript').map(r => r.id)).toEqual(['rm', 'curl'])
expect(registry.commandRulesFor('python')).toEqual([])
curl.dispose()
expect(registry.commandRulesFor('shellscript').map(r => r.id)).toEqual(['rm'])
})
it('变化事件可退订,也能放进 disposables', () => {
const registry = new CodeReaderRegistry(fakeMemento())
let changes = 0
+12 -2
View File
@@ -1,5 +1,5 @@
import type { Disposable, Event, Memento } from 'vscode'
import type { CodeReaderApi, LanguageAdapter, LibraryRule, Translator } from './contract'
import type { CodeReaderApi, CommandRule, LanguageAdapter, LibraryRule, Translator } from './contract'
/** globalState 里按 languageId 记选中的翻译插件 id:Record<languageId, translatorId>。 */
export const TRANSLATOR_CHOICE_KEY = 'superpowers.codeReader.translatorByLanguage'
@@ -7,13 +7,14 @@ export const TRANSLATOR_CHOICE_KEY = 'superpowers.codeReader.translatorByLanguag
interface Entry<T> { value: T }
/**
* 语言插件、翻译插件、库规则的注册表。内置实现和其他扩展都通过它注册(它本身就是对外的 CodeReaderApi)。
* 语言插件、翻译插件、库规则、命令规则的注册表。内置实现和其他扩展都通过它注册(它本身就是对外的 CodeReaderApi)。
* 不依赖 vscode 运行时:Disposable 和事件都是结构兼容的普通对象,便于单元测试。
*/
export class CodeReaderRegistry implements CodeReaderApi {
private readonly adapters: Entry<LanguageAdapter>[] = []
private readonly translators: Entry<Translator>[] = []
private readonly rules: Entry<LibraryRule>[] = []
private readonly commands: Entry<CommandRule>[] = []
private readonly listeners = new Set<() => void>()
constructor(private readonly state: Pick<Memento, 'get' | 'update'>) {}
@@ -41,6 +42,10 @@ export class CodeReaderRegistry implements CodeReaderApi {
return this.add(this.rules, rule)
}
registerCommandRule(rule: CommandRule): Disposable {
return this.add(this.commands, rule)
}
/** 当前语言插件覆盖的全部 languageId(去重、排序),给 when 子句用。 */
languageIds(): string[] {
return [...new Set(this.adapters.flatMap(entry => entry.value.languageIds))].sort()
@@ -61,6 +66,11 @@ export class CodeReaderRegistry implements CodeReaderApi {
return this.rules.map(entry => entry.value).filter(rule => rule.languageIds.includes(languageId))
}
/** 某 languageId 的命令规则,按注册顺序:几条规则都管同一个命令名时先注册的算。 */
commandRulesFor(languageId: string): CommandRule[] {
return this.commands.map(entry => entry.value).filter(rule => rule.languageIds.includes(languageId))
}
/** 注册着的全部库规则,按注册顺序:查规则标的引用时按 id 找。 */
libraryRules(): LibraryRule[] {
return this.rules.map(entry => entry.value)
+207 -26
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewTable, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandReading, CommandRule, CommandWord, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -51,8 +51,31 @@ export interface ZhTable {
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
*/
nodeTemplates: Record<string, TemplatePart[]>
/**
* 节点读法:节点类型 → 怎么读成一行译文,给模板排不了的写法(shell 的 `${x:-d}`、`[ -f x ]`、`case`)。
* 命中的节点只输出读法给的,不再照常翻它的子节点;读法返回 undefined 就照常翻。读法里要翻子节点用 ReaderContext.say。
*/
readers?: Record<string, NodeReader>
/** 解释器开关(SwitchFacts.id)的叫法:errexit → 出错就停;没有的写源码里的写法。 */
switches?: Record<string, string>
}
/** 读一个节点时能用的(见 ZhTable.readers)。 */
export interface ReaderContext {
document: SourceDocument
/** 照常翻一个节点(照样认读法、命令),只翻落在要翻的这一段里的部分。 */
say: (node: Node) => Inline[]
/** node 整个落在要翻的这一段里:复合语句的块只翻头部,读法按它决定写不写主体里的东西。 */
whole: (node: Node) => boolean
/** 源码里一个名字照抄、按 role 着色(不给 role 按语言插件定的词性),名字带源码下标。 */
name: (node: Node, role?: TokenRole) => Segment
/** 读一条命令(命令规则读的,没有规则时命令名照抄、各个词照常翻),连重定向;不是命令时没有。 */
command: (node: Node) => CommandReading | undefined
}
/** 一种节点的读法:一行译文,可以带一张排在这一段下面的表(分情况的「情况 / 做什么」);返回 undefined 就照常翻。 */
export type NodeReader = (node: Node, context: ReaderContext) => Inline[] | { text: Inline[], table?: PreviewTable } | undefined
interface Token {
/** 注释是空文字:不进译文,只在前后留一个空格。 */
text: string
@@ -68,14 +91,17 @@ interface Token {
group?: SegmentGroup
}
/** 跨几行写的调用排成一行、实参摊成表(见 emitCall):只排遇到的第一个,它的表记在 table 里。 */
interface CallSlot {
/**
* 排在这一段译文下面的那张表:跨几行写的调用的实参表(见 emitCall),或读法给的表(分情况的「情况 / 做什么」)。
* 一段只排一张,排了的记在 table 里。
*/
interface TableSlot {
table?: PreviewTable
}
/**
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
* calls 是这一段里跨几行写的调用要不要排成一行、实参摊成表;没有就照常翻。
* slot 是这一段下面能不能排一张表(跨几行写的调用、读法给的表);没有就照常翻。
*/
interface Collector {
document: SourceDocument
@@ -84,7 +110,7 @@ interface Collector {
tokens: Token[]
cursor: number
gap: boolean
calls?: CallSlot
slot?: TableSlot
}
const CONTEXT_SEPARATOR = ' > '
@@ -93,7 +119,7 @@ const CLOSING = new Set([')', ']', '}'])
const SEPARATORS = new Set([...CLOSING, ',', ';', ':'])
const HEADER_OPENERS = new Set(['{', ':'])
/** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */
const NAME_ROLES = new Set<TokenRole>(['variable', 'function', 'type'])
const NAME_ROLES = new Set<TokenRole>(['variable', 'constant', 'function', 'type'])
function firstLeaf(node: Node): Node {
const first = node.child(0)
@@ -194,6 +220,11 @@ const KIND_ZH: Record<BlockKind, string> = {
return: '返回',
statement: '语句',
comment: '注释',
interpreter: '解释器',
options: '选项',
case: '分情况',
command: '命令',
pipeline: '管道',
}
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
@@ -363,8 +394,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
function expandedCall(node: Node, span: SourceSpan, calls: CallSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (calls.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
if (slot.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
return undefined
const facts = call(node)
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
@@ -375,9 +406,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
* 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。
*/
function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } {
const calls: CallSlot = {}
const text = joinTokens(tokensIn(node, span, document, calls))
return { text, ...(calls.table && { table: calls.table }) }
const slot: TableSlot = {}
const text = joinTokens(tokensIn(node, span, document, slot))
return { text, ...(slot.table && { table: slot.table }) }
}
/**
@@ -408,13 +439,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
/**
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 slot 里。被调用的名字照常翻、带源码下标,
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
* 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。
*/
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
calls.table = argumentTable(facts, span, out.document)
function emitCall(out: Collector, slot: TableSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
slot.table = argumentTable(facts, span, out.document)
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
const wrapped = continued(node, out.root)
advance(out, node.startIndex)
@@ -436,6 +467,61 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
out.cursor = node.endIndex
}
/** 读法用的:照常翻、看节点在不在这一段里、照抄名字、读命令。 */
function readerContext(out: Collector, span: SourceSpan): ReaderContext {
const { document } = out
return {
document,
say: node => joinTokens(tokensIn(node, span, document)),
whole: node => node.startIndex >= span.startIndex && node.endIndex <= span.endIndex,
name: (node, role) => segment(node.text, role ?? document.language.tokenRole(node), node.startIndex),
command: (node) => {
const facts = document.language.command?.(node)
return facts && commandReading(facts, document)
},
}
}
/** 读出来的几段接着前面的词输出;读法给了表、这一段还没排表时表排在这一段下面。 */
function emitReading(out: Collector, node: Node, reading: Inline[] | { text: Inline[], table?: PreviewTable }): void {
const { text, table: below } = Array.isArray(reading) ? { text: reading, table: undefined } : reading
if (below && out.slot && !out.slot.table)
out.slot.table = below
emitPieces(out, node, text)
}
function emitPieces(out: Collector, node: Node, pieces: Inline[]): void {
advance(out, node.startIndex)
for (const piece of pieces) {
if (!('role' in piece))
emit(out, { text: '', role: 'plain', translated: true, raw: '', group: piece })
else if (piece.text)
emit(out, { text: piece.text, role: piece.role, translated: true, raw: '', ...(piece.offset !== undefined && { offset: piece.offset }) })
}
out.cursor = node.endIndex
}
/**
* 一条命令:交给命令名的命令规则读(SourceDocument.commandRule);没有规则、规则不读时命令名照抄(带源码下标)、各个词照常翻。
* 后面跟着重定向的意思(见 redirectText)。
*/
function commandReading(facts: CommandFacts, document: SourceDocument): CommandReading {
const say = (node: Node): Inline[] => expression(node, document)
const ruleFor = (name: CommandWord): CommandRule | undefined => document.commandRule?.(name.literal ?? name.node.text)
const context: CommandContext = {
displayLanguage: 'zh',
translate: say,
command: (name, args) => ruleFor(name)?.read({ name, arguments: args, redirects: [] }, context),
}
const reading = ruleFor(facts.name)?.read(facts, context)
const name = facts.name.node
const text = reading?.text ?? [
segment(name.text, document.language.tokenRole(firstLeaf(name)), name.startIndex),
...facts.arguments.flatMap(arg => [segment(' ', 'plain', undefined), ...say(arg.node)]),
]
return { text: [...text, ...facts.redirects.flatMap(redirect => redirectText(redirect, say))], ...(reading?.value && { value: true }) }
}
function collect(node: Node, span: SourceSpan, out: Collector): void {
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
return
@@ -449,10 +535,20 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
emitIteration(out, node, iteration, span)
return
}
const { calls } = out
const call = calls && expandedCall(node, span, calls, out.document)
if (calls && call) {
emitCall(out, calls, node, call, span)
const { slot } = out
const call = slot && expandedCall(node, span, slot, out.document)
if (slot && call) {
emitCall(out, slot, node, call, span)
return
}
const reading = table.readers?.[node.type]?.(node, readerContext(out, span))
if (reading) {
emitReading(out, node, reading)
return
}
const command = language.command?.(node)
if (command) {
emitPieces(out, node, commandReading(command, out.document).text)
return
}
const parts = templateParts(node, span)
@@ -486,9 +582,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
}
/** 按译文顺序收集 node 落在 span 里的词;给了 calls,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, calls?: CallSlot): Token[] {
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(calls && { calls }) }
/** 按译文顺序收集 node 落在 span 里的词;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] {
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
collect(node, span, out)
return out.tokens
}
@@ -531,13 +627,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
if (block.kind === 'comment')
return { text: translateComment(block, document) }
const calls: CallSlot = {}
const tokens = tokensIn(block.node, block.header, document, calls)
const slot: TableSlot = {}
const tokens = tokensIn(block.node, block.header, document, slot)
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
const last = tokens.at(-1)
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
tokens.pop()
return { text: joinTokens(tokens), ...(calls.table && { table: calls.table }) }
return { text: joinTokens(tokens), ...(slot.table && { table: slot.table }) }
}
function expression(node: Node, document: SourceDocument): Inline[] {
@@ -559,6 +655,11 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
translated: node => within(node, nodeSpan(node)),
within,
expanded: node => expandedText(node, clipped(nodeSpan(node)), document),
body: node => expression(node, document),
returned: (node) => {
const [value] = language.command?.(node)?.arguments ?? []
return value ? expression(value.node, document) : []
},
entryCount: (literal, size) => {
const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
@@ -580,6 +681,10 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return exceptionRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
case 'interpreter':
return interpreterRows(block.facts)
case 'options':
return optionsRows(block.facts, table.switches ?? {})
default:
return variableRows(block, phrases)
}
@@ -659,6 +764,10 @@ interface Phrases {
within: (node: Node, span: SourceSpan) => Inline[]
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
body: (node: Node) => Inline[]
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
returned: (node: Node) => Inline[]
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
entryCount: (literal: Node, size: number) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */
@@ -675,6 +784,31 @@ function hint(text: string): Segment {
return { text, role: 'hint' }
}
const STREAM_ZH: Record<'output' | 'error' | 'both', string> = { output: '输出', error: '错误输出', both: '全部输出' }
/**
* 重定向的意思,括号里淡色写在命令后面:`>&2` 打印到错误输出,`2>&1` 错误输出并进输出,`2>/dev/null` 丢掉错误输出,
* `> f` 写到 f,`>> f` 追加到 f,`< f` 从 f 读。
*/
function redirectText(redirect: Redirect, say: (node: Node) => Inline[]): Inline[] {
const note = (...parts: Inline[]): Inline[] => [hint('('), ...parts, hint(')')]
switch (redirect.kind) {
case 'merge':
if (redirect.stream === redirect.into)
return []
return note(hint(redirect.into === 'error' ? '打印到错误输出' : '错误输出并进输出'))
case 'read':
return note(hint('从 '), ...say(redirect.source), hint(' 读'))
default: {
const stream = STREAM_ZH[redirect.stream]
if (redirect.target.text === '/dev/null')
return note(hint(`丢掉${stream}`))
const where = redirect.kind === 'append' ? '追加到 ' : '写到 '
return note(hint(redirect.stream === 'output' ? where : `${stream}${where}`), ...say(redirect.target))
}
}
}
/** 布尔属性的一行:没有这个概念的语言(facts 里没填)没有这一行。 */
function flagRows(attribute: 'async' | 'abstract' | 'typeOnly', value: boolean | undefined): PropertyRow[] {
return value === undefined ? [] : [{ label: ATTRIBUTE_ZH[attribute], value: [{ text: YES_NO[`${value}`], role: 'storage' }] }]
@@ -728,11 +862,58 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
...accessRows(facts.access),
...flagRows('async', facts.async),
...decoratorRows(facts.decorators, say),
parameterRow('输入参数', facts.parameters, say),
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
...(facts.positional
? positionalRows(facts, facts.positional, say)
: [
parameterRow('输入参数', facts.parameters, say),
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]),
]
}
/**
* 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N);
* 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。
*/
function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases): PropertyRow[] {
const inputs: PropertyRow = positional.length === 0
? { label: '输入参数', value: [hint('无')] }
: {
label: '输入参数',
value: [hint(`${positional.length} 个,按位置`)],
columns: ['位置', '函数里叫'],
items: positional.map(param => ({
cells: [[hint(`第 ${param.position} 个`)], param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)]],
...defining(param.name),
})),
}
const prints = facts.prints ?? []
const captured = facts.captured && facts.name ? [hint(`(调用处用 $(${facts.name.text} …) 拿到)`)] : []
const output: PropertyRow[] = prints.length > 0 ? [{ label: '输出', value: [...joined(prints.map(say.body), '\n'), ...captured] }] : []
const returns = facts.returns ?? []
const values = returns.map(say.returned).filter(value => value.length > 0)
const result: PropertyRow[] = values.length > 0
? [{ label: '返回', value: joined(values, '、') }]
: prints.length === 0 ? [{ label: '返回', value: [hint('最后一句成不成功')] }] : []
return [inputs, ...output, ...result]
}
/** 解释器:叫什么,怎么找到它(按 PATH 找,或写死的路径)。 */
function interpreterRows(facts: InterpreterFacts): PropertyRow[] {
return [
{ label: '解释器', value: [segment(facts.program, 'function', undefined)] },
{ label: '怎么找', value: [hint(facts.lookup === 'env' ? `按 PATH 去找(${facts.command})` : `写死的路径(${facts.command})`)] },
]
}
/** 解释器开关:每个开关一行,属性名是开关的意思(没有叫法的写源码里的写法),值是开没开,后面淡色写源码里的写法。 */
function optionsRows(facts: OptionsFacts, names: Record<string, string>): PropertyRow[] {
return facts.switches.map(item => ({
label: names[item.id] ?? item.flag,
value: [{ text: YES_NO[`${item.on}`], role: 'storage' }, { text: ' ', role: 'plain' }, hint(item.flag)],
}))
}
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
return {