✨ 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

+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
}