✨ feat(vscode): 代码阅读契约加命令规则(按命令名注册、读一条命令)、按位置的参数 / 输出 / 返回、解释器和选项块、分情况 / 命令 / 管道种类、常量色;模板引擎加节点读法;没有定义、高亮提供者的语言用语言插件认的同一文件里的名字跳转、高亮
This commit is contained in:
1 parent
cf09c24730
commit
ff2a44f080
17 files changed
+555
-60
No files matched your search
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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: [] }
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
}))
|
||||
}
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user