✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新
This commit is contained in:
1 parent
ebf228cdc7
commit
ecc415d763
30 files changed
+1033
-157
No files matched your search
@@ -41,6 +41,7 @@
|
||||
| 规则自己查定义 | 规则可以把自己给的名字标成「引用」(Celery 任务表里的任务名),⌘ / Ctrl 悬停、点击这样的名字时由规则查定义(Celery 在整个工作区里找任务函数),其余名字照旧问语言扩展;webview 只有一条链接 / 跳转的路,样子、闪、挑选框、在预览里打开都一样。规则要在全工作区找东西时只描述要找什么(`WorkspaceIndex`),找文件、解析、缓存、跟着改动更新归宿主。见「规则自己查定义」 |
|
||||
| 中文名 | 代码里给定义起的中文名(`Annotated[T, Comment("当前用户")]`、`@Comment("…")`、行尾 `# @Comment 当前用户`,没有时取参数在 docstring / JSDoc 里的说明):预览里这个定义和用到它的各处都显示中文名,悬停看原名,定义处本身在中文名后面淡淡写原名。语言插件认写法,用到它的各处由语言扩展按语义认(同一文件问 document highlights,别的文件定义的先查工作区索引、再问定义提供者),webview 只按源码下标换字。见「中文名」 |
|
||||
| 推导式(写在表达式里的遍历) | Python 的列表 / 集合 / 字典推导式、生成器表达式出现在译文任何地方(一行译文、属性表的值、默认值、字典项的值),都原地换成一个带循环图标和结果类型(生成器 / 列表 / 集合 / 字典)的小框,颜色同「循环」块,框里按读的顺序重排:「遍历 来源 的每个 变量,仅当 条件,取 值」。语言插件报结构,翻译插件定用词,webview 只认「框」不认语言 |
|
||||
| 调用与构造 | 实参列表跨了几行写的调用不挤成一长行:写成「[等待 ]新建 / 调用 被调用的名字 · N 个参数」,实参摊成一张表「参数 \| 值」(按名字传的写名字,按位置传的写「第 N 个」,`*x` / `...x` 写「展开」,`**x` 写「展开关键字」);一行写完的调用照旧一行译文。新建还是调用:TypeScript 看有没有 `new`;Python 写法一样,查被调用的名字的定义,是类就是「新建」,是函数、查不到都是「调用」,先按调用画、查到了再刷新。表在变量 / 常量属性表的初始值 / 值那一行下面(能折叠),在返回、语句这些一行译文的块里排在那一行下面。语言插件报结构,翻译插件定用词,webview 只画表。见「调用与构造」 |
|
||||
|
||||
## 用户可见行为
|
||||
|
||||
@@ -133,7 +134,7 @@
|
||||
- 悬停提示(整个插件的网页只走这一条路,预览和看板共用同一个组件):带提示的元素只写 `data-tooltip="提示"`(名字照旧写在 `aria-label` 里,只有图标的按钮必须有 `aria-label`),页面根上挂一个 `TooltipHost`(`webview-ui/src/components/ui/tooltip.tsx`)统一弹,不用原生 `title`;提示里的换行照样换行(看板摘修复弹框里「迁移」列出的几个文件一行一个);注释、回答里 Markdown 链接的标题(`[文字](网址 "标题")`)也写成 `data-tooltip`。网页的 TSX 里写原生 `title` 属性是 lint 错误(`vscode/eslint.config.mjs`),只有 `iframe` 这种必须带标题的元素例外,组件自己叫 `title` 的属性不算。原因:macOS 上 VS Code 带的 Electron(1.139 是 Electron 43)只在鼠标压着带 `title` 的元素自身边缘时才弹原生提示,停在它里面的图标、文字上不弹(electron/electron#49843,Electron 38 起),种类图标 20px 的框几乎被 16px 的图形占满,基本弹不出来;样子也是系统的。样子照 VS Code 的悬停框:`--vscode-editorHoverWidget-background` 底、`--vscode-editorHoverWidget-border` 边框、`--vscode-editorHoverWidget-foreground` 字、`0 2px 8px --vscode-widget-shadow` 阴影,11px 字,圆角 4px,最宽 22rem;优先在元素上方,上方放不下翻到下方,水平对着元素中间,贴边留 8px,不接鼠标。鼠标停 500ms 才弹,在同一个元素里移动不重新计时,已经弹着时移到另一个带提示的元素上立刻换;移开、移出页面、按下鼠标、按任意键(含 Esc)、滚动就收,收了之后鼠标不离开这个元素不再弹。键盘把焦点移到带提示的元素上(`:focus-visible`)立刻弹,焦点离开就收;提示不是元素自己的名字(`aria-label` 或它的字)时用 `aria-describedby` 挂到元素上,读屏当描述念。
|
||||
- 种类图标和底色按种类区分(颜色见「颜色」);注释块虚线框。
|
||||
- 内容两种状态,每块单独切:
|
||||
- 预览:函数、类、枚举、异常、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文的块带说明(Python 字段的属性 docstring)时,说明按 Markdown 排在译文下面。
|
||||
- 预览:函数、类、枚举、异常、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面。
|
||||
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。
|
||||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。
|
||||
- 库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。
|
||||
@@ -256,6 +257,30 @@
|
||||
|
||||
- TypeScript 没有这种写法,不涉及。几种读不成遍历的写法(语法树认、Python 3 不认的 `for x in a, b`,写到一半缺了变量 / 来源 / 条件 / 值的)照常逐词直译。
|
||||
|
||||
### 调用与构造
|
||||
|
||||
原型画板「调用 · 构造」(`code.js` 的「调用与构造」一节)。实参多的调用一般跨几行写,挤成一行译文读不下去;把实参摊成一张表。
|
||||
|
||||
- 哪些调用:实参列表(括号连同里面,`CallFacts.argumentList`)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScript `describe('x', () => { … })` 这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(`items\n .filter(…)\n .map(…)`)也不算。一段里只排遇到的第一个(先外后里),实参里的调用不再排成表,照常一行译文。
|
||||
- 一行:`[等待 ]新建 / 调用 被调用的名字 · N 个参数`。「等待」就是 `await` 照常的译法(在调用前面),「新建」「调用」按声明关键字着色;被调用的名字照常翻、带源码下标(⌘ / Ctrl + 点击、单击高亮、中文名都照旧),前面几节(对象、模块)按变量着色,最后那个名字新建时按类型着色、调用时按函数着色;「· N 个参数」按说明词着色,N 数的是表里的行。
|
||||
- 新建还是调用:写法说明了的看写法(`CallFacts.construct`:TypeScript 的 `new X(…)` 是新建,不带 new 的是调用,都不问定义);Python 新建对象和调用函数写法一样,翻译插件问 `SourceDocument.calleeKind`:名字定义成类(含异常、枚举)是「新建」,函数、别的、查不到、还没查到都是「调用」。两种语言都用「新建」这个词。
|
||||
- 查定义(宿主,`calleeKinds.ts` + `editor/definitions.ts` 的 `definitionKind`):排视图数据是同步的,先按已经查到的排(第一次都按调用画),记下问了哪些调用;发出预览后问语言扩展被调用的名字(`CallFacts.name`)定义在哪(和 ⌘ + 点击同一套),打开定义所在的文件、用它那种语言的语言插件分块(`readDefinitionKind`:自己的名字盖住定义处的最里层那块),几处定义取第一处认得出的。查到的和已知的不一样就重排、重发。按被调用的表达式原文记(`user_service.update_user`),同一份文档里写法一样的按一处查;文档改了先用上一版查到的,每个版本查一次,语言扩展还没起来(没答)的照 RETRY_DELAYS 隔一会儿再查(和库规则查名字的定义同一套 `DeferredAnswers`)。被调用的不是名字(`handlers[k](…)`)的不查,按调用算。
|
||||
- 表:两列「参数 | 值」,每个实参一行、照源码顺序。按名字传的(Python `name=value`)写名字,按变量着色、带源码下标;按位置传的写「第 N 个」(说明词,只数按位置传的);`*args`、`...args` 写「展开」,`**kwargs` 写「展开关键字」(说明词)。值照常一行译文(名字带下标),里面的调用不再展开;唯一的实参是生成器时(`sum(\n x for x in xs\n)`),共用的那对括号属于调用,格子里只有框。表的样子同输入参数表(`PropertyTable`:表头、圆角外框、格子之间分隔线,第一列按内容定宽)。
|
||||
- 放在哪:
|
||||
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(`PropertyRow.columns = ['参数', '值']`),带折叠箭头、默认展开。值是字典 / 对象字面量时照旧按「字典 N 项」展开,不看里面的调用。
|
||||
- 一行译文的块(返回、语句、字段……):译文里调用的位置换成那一行字(「返回 新建 UserDetailResponse · 11 个参数」),表排在译文下面(`TextTranslation.table` → 视图数据的 `table`),总是展开;块带说明时说明排在表下面。
|
||||
- 分工:语言插件报结构(`LanguageAdapter.call`:被调用的表达式、最后那个名字、写法是不是新建、实参列表、各个实参),翻译插件定用词、挑哪个调用、怎么排;webview 只画表(`PreviewTable`),不认语言;跳到定义、高亮各处出现、中文名在表里照旧(`renderedNames`、`reveal.ts` 都算上这张表)。
|
||||
- 样例(`fixtures/routes.py`,luffy 的用户路由):
|
||||
|
||||
| 源码 | 预览 |
|
||||
| --- | --- |
|
||||
| `return UserDetailResponse(\n id=user.id,\n …(11 个)\n)`(定义是类) | 一行 `返回 新建 UserDetailResponse · 11 个参数`,表第一行 `id \| user.id` |
|
||||
| `user = await user_service.update_user(\n db,\n user_id=user_id,\n …(10 个)\n)` | 初始值 `等待 调用 user_service.update_user · 10 个参数`,表第一行 `第 1 个 \| db`、第二行 `user_id \| user_id` |
|
||||
| `user = await user_service.get_user(db, user_id=user_id)` | 初始值 `等待 user_service.get_user(db, user_id=user_id)`(一行写完,不展开) |
|
||||
| `payload = await file.read()` | 初始值 `等待 file.read()` |
|
||||
| `const url = new URL(\n \`…/comments\`,\n)` | 初始值 `新建 URL · 1 个参数`,表 `第 1 个 \| \`…/comments\`` |
|
||||
| `return concatAll(\n base,\n ...extra,\n)` | 一行 `返回 调用 concatAll · 2 个参数`,表 `第 1 个 \| base`、`展开 \| extra` |
|
||||
|
||||
### 注释的 Markdown 排版
|
||||
|
||||
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
|
||||
@@ -499,7 +524,23 @@ export interface IterationFacts {
|
||||
clauses: IterationClause[] // 至少一节,第一节是 for
|
||||
value: Node // 每一项取的值
|
||||
key?: Node // 结果是字典时每一项的键
|
||||
sharedParentheses?: { open: Node, close: Node } // 遍历的括号同时是外面调用的括号(Python f(x for x in y)):写在遍历外面
|
||||
sharedParentheses?: { open: Node, close: Node } // 遍历的括号同时是外面调用的括号(Python f(x for x in y)):写在遍历外面;只翻这个遍历(它是实参表里的值)时不写
|
||||
}
|
||||
|
||||
/** 调用里的一个实参,照源码顺序;注释不算。 */
|
||||
export type CallArgument =
|
||||
| { kind: 'positional', value: Node } // 按位置传的
|
||||
| { kind: 'keyword', name: Node, value: Node } // 按名字传的:Python 的 name=value
|
||||
| { kind: 'spread', value: Node } // *args、...args
|
||||
| { kind: 'keywordSpread', value: Node } // Python 的 **kwargs
|
||||
|
||||
/** 一次调用(含新建对象)的结构:语言插件读出,翻译插件把跨几行写的排成「新建 / 调用 名字 · N 个参数」一行、实参摊成表(见「调用与构造」)。 */
|
||||
export interface CallFacts {
|
||||
callee: Node // 被调用的表达式(TypeScript 的 new 不算在里面)
|
||||
name?: Node // callee 里最后那个名字(点分名字的最后一节):着色、查定义用它;callee 不是名字时没有
|
||||
construct?: boolean // 写法本身说明了是不是新建对象时给(TypeScript:new 是,别的调用不是);Python 不给,翻译插件问 SourceDocument.calleeKind
|
||||
argumentList: Node // 括号连同里面的实参:它跨了几行,这次调用就是跨几行写的
|
||||
arguments: CallArgument[]
|
||||
}
|
||||
|
||||
export interface DeclaratorFacts {
|
||||
@@ -610,6 +651,12 @@ export interface PropertyItem extends Defines { cells: Inline[][], items?: Prope
|
||||
*/
|
||||
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] }
|
||||
|
||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
|
||||
export interface PreviewTable { columns: string[], items: PropertyItem[] }
|
||||
|
||||
/** 一行译文的块翻出来的:一行译文(注释块是 Markdown 说明);跨几行写的调用还有摊开各个实参的表,预览排在这一行下面。 */
|
||||
export interface TextTranslation { text: PreviewText, table?: PreviewTable }
|
||||
|
||||
/** 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。 */
|
||||
export interface LanguageAdapter {
|
||||
id: string
|
||||
@@ -623,6 +670,8 @@ export interface LanguageAdapter {
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
|
||||
call?: (node: Node) => CallFacts | undefined
|
||||
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
|
||||
displayNames?: DisplayNameReader
|
||||
}
|
||||
@@ -642,7 +691,16 @@ export interface DisplayNameReader {
|
||||
}
|
||||
|
||||
/** 翻译插件翻一块时拿到的这份源码:源码字符串,解析它的语言插件,和要从注释、说明里去掉的记号。 */
|
||||
export interface SourceDocument { source: string, language: LanguageAdapter, markers: SourceRange[] }
|
||||
export interface SourceDocument {
|
||||
source: string
|
||||
language: LanguageAdapter
|
||||
markers: SourceRange[]
|
||||
/**
|
||||
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
|
||||
* 同步地只交出已经查到的;还没查到、查不到、宿主不查都是 undefined,查到了宿主重排、重发(见「调用与构造」)。
|
||||
*/
|
||||
calleeKind?: (call: CallFacts) => BlockKind | undefined
|
||||
}
|
||||
|
||||
/** 筛选属性和值的显示名;没给名字的显示 id。 */
|
||||
export interface AttributeNames {
|
||||
@@ -656,7 +714,7 @@ export interface Translator {
|
||||
label: string // '中文直译'
|
||||
languageIds: string[]
|
||||
displayLanguage: string // 译文用的语言('zh'):库规则按它挑自己的词表
|
||||
translate: (block: TextBlock, document: SourceDocument) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
|
||||
translate: (block: TextBlock, document: SourceDocument) => TextTranslation // 只翻 header 那一段;注释块给 Markdown 原文;跨几行写的调用带实参表
|
||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
|
||||
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
|
||||
doc: (node: Node, document: SourceDocument) => Markdown | undefined // 说明文字(注释、docstring、当说明用的字符串)→ Markdown 原文,同属性表的「说明」
|
||||
@@ -750,9 +808,9 @@ export interface CodeReaderApi {
|
||||
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
|
||||
- 内置的 TypeScript、Python 语言插件、两个中文直译翻译插件和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。
|
||||
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。
|
||||
- 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。
|
||||
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `displayNames` 就没有中文名。
|
||||
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `call` 的调用不摊成表,没有 `displayNames` 就没有中文名。
|
||||
- 中文名的分工:语言插件只认写法(`DisplayNameReader.find`:定义处的名字、中文名、注释里的记号);宿主解析一次源码时顺带问它,记号放进 `SourceDocument.markers` 交给翻译插件(翻注释、说明时去掉,中文模板直译在 `ZhTable.commentText` 之前去掉),定义交给预览找用到它的各处(`nameResolution.ts`);翻译插件不认识 `@Comment`,webview 不认识任何语言。
|
||||
- 翻译插件拿到的:代码块(kind、node、header、children,属性表块还有 facts)和 `SourceDocument`。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。
|
||||
|
||||
@@ -1209,7 +1267,7 @@ export interface CodeReaderApi {
|
||||
| TypeScript | `ternary_expression`(`c ? a : b`) | 如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 |
|
||||
| Python | `conditional_expression`(`a if c else b`,没有字段名,具名子节点依次是 a、c、b) | 如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 |
|
||||
|
||||
写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `iteration`(翻译时从 `SourceDocument.language` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。
|
||||
写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 `iteration`(翻译时从 `SourceDocument.language` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。跨几行写的调用也不走节点模板:由语言插件的 `call` 认出来,引擎只在翻一行译文(`translate`)和变量 / 常量的值(`declaredValue`)时按「调用与构造」一节排(`emitCall`、`argumentTable`),属性表别的值、库规则借的 `expression` 照常一行。
|
||||
|
||||
TypeScript 关键字表(流程 = keyword,声明 = storage):
|
||||
|
||||
@@ -1277,9 +1335,10 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 属性表块(有 facts)和库规则认领的块只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。
|
||||
* 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文(Translator.doc),webview 排在译文下面。
|
||||
* 一行译文里有跨几行写的调用时 table 是摊开实参的表(TextTranslation.table),块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文(Translator.doc),
|
||||
* webview 依次排在译文下面。
|
||||
*/
|
||||
type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
```
|
||||
|
||||
- 扩展 → webview:
|
||||
@@ -1351,13 +1410,16 @@ vscode/src/codeReader/
|
||||
rules/enum.ts 内置 enum 库规则(Python):枚举类认成枚举块,成员照 Python 的规则认
|
||||
rules/dataclass.ts 内置 dataclasses 库规则(Python):数据类卡片,字段照 dataclasses 的规则认
|
||||
rules/fastapi.ts 内置 FastAPI 库规则(Python):路由器卡片(接口表)、接口卡片(请求表、响应表),参数照 FastAPI 的规则放,异常类的状态码、类型别名、枚举经索引查
|
||||
ruleLookups.ts 库规则认领时查别的文件里的定义(RuleContext.lookup):先交出已知的、记下问了什么,发出预览后查(索引 + 语言扩展查定义),变了叫宿主重发(不碰 VS Code)
|
||||
ruleLookups.ts 库规则认领时查别的文件里的定义(RuleContext.lookup):索引 + 语言扩展查定义(不碰 VS Code)
|
||||
calleeKinds.ts 调用的名字定义成哪种块(SourceDocument.calleeKind):按被调用的表达式原文记、问最后那个名字(不碰 VS Code)
|
||||
deferredAnswers.ts 上面两样共用的:排视图数据时先交出已知的、记下问了什么,发出预览后按版本查一次、没答的隔一会儿再查,变了叫宿主重发(不碰 VS Code)
|
||||
(blockView.ts 的 readDefinitionKind:定义处落在哪块自己的名字上;editor/definitions.ts 的 definitionKind:打开定义所在的文件交给它)
|
||||
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字)
|
||||
rules/segments.ts 库规则共用的拼属性表文字的办法
|
||||
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点)
|
||||
languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数)
|
||||
languages/typescript.ts (TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
|
||||
languages/python.ts (iteration:推导式的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
|
||||
languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
|
||||
languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
|
||||
languages/displayNames.ts 中文名的共用部分:第一小句、行尾记号、按优先级汇总
|
||||
languages/pythonNames.ts Python 的中文名写法(Annotated、@Comment、行尾记号、docstring)
|
||||
languages/typescriptNames.ts TypeScript / JavaScript 的中文名写法(@Comment 装饰器、行尾记号、JSDoc)
|
||||
@@ -1413,6 +1475,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。
|
||||
- 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
|
||||
- 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。
|
||||
- 调用与构造:语言插件读出的结构(Python 按位置 / 按名字 / `*x` / `**x`、唯一的生成器实参、被调用的不是名字时没有 name、装饰器和父类列表不算;TypeScript `new` 是新建、被调用的不含 new、`...x`、带标签的模板字符串和不带括号的 `new X` 不算);中文直译按「调用与构造」一节的样例:`routes.py` 的 `return UserDetailResponse(…)` 定义是类(异常也算)时「返回 新建 UserDetailResponse · 11 个参数」、11 行表、名字带下标,没查到 / 查不到 / 是函数时「调用」、名字按函数着色;`update_user` 的初始值「等待 调用 user_service.update_user · 10 个参数」、表是这一行的子行(列名 参数 / 值)、第一行「第 1 个」按说明词着色;一行写完的 `get_user(...)`、`file.read()` 照旧一行、不问定义;「展开」「展开关键字」、「第 N 个」只数按位置传的、实参里的调用不再排成表;`fixtures/calls.ts.txt`(本仓库的代码)new 是新建、不带 new 是调用、都不问定义;链式调用跨了几行、回调主体是子块的调用不排。`calleeKinds.test.ts`:按表达式原文记、同一版本不再查、被调用的不是名字不问、查了没有不算变、没答的隔一会儿只再查没答的。视图数据带 `table`、`readDefinitionKind` 认类 / 函数 / 异常 / 变量、`renderedNames` 算上表里的名字、`documentMessage` 把 calleeKind 交给翻译插件;webview 跳到定义、键盘跳到高亮的各处出现都认一行译文下面那张表。
|
||||
- 只经契约:只照 contract 写的假语言插件(`iteration` 把一个普通调用认成遍历)配中文模板,那个调用换成遍历框;同一个翻译插件配上不给 `iteration` 的语言插件照常翻。测试文件不 import 任何语言插件模块。
|
||||
- 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(`tableTemplate`)只看列数。
|
||||
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { LanguageAdapter, Translator } from './contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readBlockViews, toBlockViews } from './blockView'
|
||||
import { readBlockViews, readDefinitionKind, toBlockViews } from './blockView'
|
||||
import { replaceLines } from './editText'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
@@ -257,4 +257,25 @@ describe('blockView', () => {
|
||||
])
|
||||
expect(translationText(list[0].children[0])).toBe('返回 非 x')
|
||||
})
|
||||
|
||||
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 returned = list[0].children[0]
|
||||
expect(translationText(returned)).toBe('返回 新建 Detail · 1 个参数')
|
||||
expect('table' in returned && returned.table).toMatchObject({
|
||||
columns: ['参数', '值'],
|
||||
items: [{ cells: [[{ text: 'id', offset: source.indexOf('id=') }], [{ text: 'user' }, { text: '.' }, { text: 'id' }]] }],
|
||||
})
|
||||
})
|
||||
|
||||
it('readDefinitionKind:offset 落在哪块自己的名字上就是那块的种类,里层的块优先;不在名字上没有', async () => {
|
||||
const source = 'class UserService:\n def update_user(self):\n pass\n\nclass NotFound(Exception):\n pass\n\nlimit = 3\n'
|
||||
const kind = (text: string): Promise<string | undefined> => readDefinitionKind(runtime, py, 'python', source, source.indexOf(text))
|
||||
expect(await kind('UserService')).toBe('class')
|
||||
expect(await kind('update_user')).toBe('function')
|
||||
expect(await kind('NotFound')).toBe('exception')
|
||||
expect(await kind('limit')).toBe('variable')
|
||||
expect(await kind('pass')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockAttributes, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, Markdown, Node, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
|
||||
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 { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
@@ -33,9 +33,9 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
|
||||
* 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文,预览排在译文下面。
|
||||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(TextTranslation.table),块的说明(TextBlock.doc,翻译插件给的 Markdown 原文)。
|
||||
*/
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
|
||||
interface Line { start: number, end: number }
|
||||
|
||||
@@ -159,10 +159,13 @@ export function toBlockViews(blocks: CodeBlock[], document: SourceDocument, tran
|
||||
codeTokens: colorLines(root, source, lines, startRow, endRow, indent, adapter.tokenRole),
|
||||
}
|
||||
}
|
||||
/** 一行译文,带说明的块连着说明。 */
|
||||
const textView = (block: TextBlock): { translation: PreviewText, doc?: Markdown } => {
|
||||
const doc = block.doc && translator?.doc(block.doc, document)
|
||||
return { translation: translator ? translator.translate(block, document) : [], ...(doc && { doc }) }
|
||||
/** 一行译文,连着摊开实参的表、块的说明(有才有)。 */
|
||||
const textView = (block: TextBlock): { translation: PreviewText, table?: PreviewTable, doc?: Markdown } => {
|
||||
if (!translator)
|
||||
return { translation: [] }
|
||||
const { text, table } = translator.translate(block, document)
|
||||
const doc = block.doc && translator.doc(block.doc, document)
|
||||
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
|
||||
}
|
||||
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
|
||||
const anchors = new BlockAnchors()
|
||||
@@ -215,7 +218,10 @@ export interface DocumentViews {
|
||||
displayNames: DisplayName[]
|
||||
}
|
||||
|
||||
/** 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。 */
|
||||
/**
|
||||
* 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。
|
||||
* calleeKind 交出调用的名字的定义眼下查到是哪种块(SourceDocument.calleeKind)。
|
||||
*/
|
||||
export async function readBlockViews(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
adapter: LanguageAdapter,
|
||||
@@ -223,14 +229,46 @@ export async function readBlockViews(
|
||||
languageId: string,
|
||||
source: string,
|
||||
rules: RuleRun = NO_RULES,
|
||||
calleeKind?: SourceDocument['calleeKind'],
|
||||
): Promise<DocumentViews> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
|
||||
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), { source, language: adapter, markers }, translator, rules)
|
||||
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 }
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** 块树里自己的名字(见 ownName)盖住 offset 的最里层那块的种类;offset 不在哪块的名字上时没有。 */
|
||||
export function blockKindAt(blocks: readonly CodeBlock[], offset: number): BlockKind | undefined {
|
||||
for (const block of blocks) {
|
||||
const inner = blockKindAt(block.children, offset)
|
||||
if (inner)
|
||||
return inner
|
||||
const name = ownName(block)
|
||||
if (name && name.startIndex <= offset && offset < name.endIndex)
|
||||
return block.kind
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 解析一份源码,offset 处的名字定义的是哪种块(见 blockKindAt);语法树用完即释放。 */
|
||||
export async function readDefinitionKind(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
adapter: LanguageAdapter,
|
||||
languageId: string,
|
||||
source: string,
|
||||
offset: number,
|
||||
): Promise<BlockKind | undefined> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
return blockKindAt(adapter.toBlocks(tree.rootNode, source, languageId), offset)
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { BlockKind, CallFacts, Node } from './contract'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DocumentCallees } from './calleeKinds'
|
||||
|
||||
/** 一次调用:只用到被调用的表达式原文,和最后那个名字的下标。 */
|
||||
function call(callee: string, nameAt?: number): CallFacts {
|
||||
const facts = { callee: { text: callee } as Node, argumentList: {} as Node, arguments: [] }
|
||||
return nameAt === undefined ? facts : { ...facts, name: { startIndex: nameAt } as Node }
|
||||
}
|
||||
|
||||
/** 各下标处名字的定义是哪种块(语言扩展答的;没有的是没答);记下问了哪些下标。 */
|
||||
function source(kinds: Record<number, BlockKind | null>): { asked: number[], kindOf: (offset: number) => Promise<BlockKind | null | undefined> } {
|
||||
const asked: number[] = []
|
||||
return {
|
||||
asked,
|
||||
kindOf: async (offset) => {
|
||||
asked.push(offset)
|
||||
return kinds[offset]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('调用的名字定义成哪种块(DocumentCallees)', () => {
|
||||
it('第一遍当没查到,按被调用的表达式原文记下问了什么(写法一样的按一处问);查到后叫宿主重排,再排时交出来', async () => {
|
||||
let changed = 0
|
||||
const callees = new DocumentCallees(() => changed++, [])
|
||||
const first = callees.session()
|
||||
expect(first.calleeKind(call('UserDetailResponse', 40))).toBeUndefined()
|
||||
expect(first.calleeKind(call('UserDetailResponse', 90))).toBeUndefined()
|
||||
expect(first.calleeKind(call('user_service.update_user', 120))).toBeUndefined()
|
||||
expect(first.requests()).toEqual([{ callee: 'UserDetailResponse', offset: 40 }, { callee: 'user_service.update_user', offset: 120 }])
|
||||
|
||||
const from = source({ 40: 'class', 120: 'function' })
|
||||
callees.resolve(1, first.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(changed).toBe(1))
|
||||
const second = callees.session()
|
||||
expect(second.calleeKind(call('UserDetailResponse', 12))).toBe('class')
|
||||
expect(second.calleeKind(call('user_service.update_user', 7))).toBe('function')
|
||||
|
||||
// 同一版本再发一遍:问过的不再查
|
||||
callees.resolve(1, first.requests(), from.kindOf)
|
||||
await Promise.resolve()
|
||||
expect(from.asked).toEqual([40, 120])
|
||||
})
|
||||
|
||||
it('被调用的不是名字(没有 name)不问;查了没有(null)不算变了', async () => {
|
||||
let changed = 0
|
||||
const callees = new DocumentCallees(() => changed++, [])
|
||||
const session = callees.session()
|
||||
expect(session.calleeKind(call('handlers[k]'))).toBeUndefined()
|
||||
session.calleeKind(call('build', 5))
|
||||
expect(session.requests()).toEqual([{ callee: 'build', offset: 5 }])
|
||||
const from = source({ 5: null })
|
||||
callees.resolve(1, session.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(from.asked).toEqual([5]))
|
||||
await Promise.resolve()
|
||||
expect(changed).toBe(0)
|
||||
})
|
||||
|
||||
it('语言扩展还没起来(没答):隔一会儿只再查没答的,查到了再叫重排', async () => {
|
||||
let changed = 0
|
||||
const pending: (() => void)[] = []
|
||||
const scheduled: number[] = []
|
||||
const callees = new DocumentCallees(() => changed++, [5, 10], (task, ms) => {
|
||||
scheduled.push(ms)
|
||||
pending.push(task)
|
||||
return setTimeout(() => {}, 0)
|
||||
})
|
||||
const session = callees.session()
|
||||
session.calleeKind(call('Detail', 40))
|
||||
session.calleeKind(call('run', 60))
|
||||
const kinds: Record<number, BlockKind | null> = { 60: 'function' }
|
||||
const from = source(kinds)
|
||||
callees.resolve(1, session.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(scheduled).toEqual([5]))
|
||||
expect(changed).toBe(1)
|
||||
|
||||
kinds[40] = 'class'
|
||||
pending.shift()?.()
|
||||
await vi.waitFor(() => expect(changed).toBe(2))
|
||||
expect(from.asked).toEqual([40, 60, 40])
|
||||
expect(callees.session().calleeKind(call('Detail', 40))).toBe('class')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { BlockKind, CallFacts } from './contract'
|
||||
import { DeferredAnswers } from './deferredAnswers'
|
||||
|
||||
/**
|
||||
* 调用的名字的定义是哪种块(SourceDocument.calleeKind):翻译插件靠它分新建对象还是调用函数。
|
||||
* 按被调用的表达式原文记(`user_service.update_user`),同一份文档里写法一样的调用按一处查;问语言扩展时问它最后那个名字。这里不碰 VS Code。
|
||||
*/
|
||||
|
||||
/** 问的一次调用:被调用的表达式原文,和它最后那个名字在源码里的下标。 */
|
||||
export interface CalleeRequest { callee: string, offset: number }
|
||||
|
||||
/** 这份文档里 offset 处名字的定义是哪种块:查了没有是 null,语言扩展没答(还没起来)是 undefined。 */
|
||||
export type CalleeSource = (offset: number) => Promise<BlockKind | null | undefined>
|
||||
|
||||
/** 排一遍视图数据用的:交给翻译插件的 calleeKind,和这一遍问了哪些调用。 */
|
||||
export interface CalleeSession {
|
||||
calleeKind: (call: CallFacts) => BlockKind | undefined
|
||||
requests: () => CalleeRequest[]
|
||||
}
|
||||
|
||||
/** 一份打开着的文档里调用的名字查到定义成哪种块;什么时候查、查到了怎么叫宿主重排见 DeferredAnswers。 */
|
||||
export class DocumentCallees {
|
||||
private readonly answers: DeferredAnswers<CalleeRequest, BlockKind>
|
||||
|
||||
constructor(changed: () => void, delays?: readonly number[], schedule?: (run: () => void, ms: number) => ReturnType<typeof setTimeout>) {
|
||||
this.answers = new DeferredAnswers(({ callee }) => callee, (a, b) => (a ?? null) === (b ?? null), changed, delays, schedule)
|
||||
}
|
||||
|
||||
/** 被调用的不是名字(没有 CallFacts.name)的不问,按调用算。 */
|
||||
session(): CalleeSession {
|
||||
const session = this.answers.session()
|
||||
return {
|
||||
calleeKind: call => call.name ? session.ask({ callee: call.callee.text, offset: call.name.startIndex }) : undefined,
|
||||
requests: session.questions,
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版问的调用:这个版本查过的不再查。 */
|
||||
resolve(version: number, requests: readonly CalleeRequest[], source: CalleeSource): void {
|
||||
this.answers.resolve(version, requests, ({ offset }) => source(offset))
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.answers.dispose()
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,32 @@ export interface IterationFacts {
|
||||
sharedParentheses?: { open: Node, close: Node }
|
||||
}
|
||||
|
||||
/** 调用里的一个实参,照源码顺序;注释不算。 */
|
||||
export type CallArgument
|
||||
= | { kind: 'positional', value: Node } // 按位置传的
|
||||
| { kind: 'keyword', name: Node, value: Node } // 按名字传的:Python 的 name=value
|
||||
| { kind: 'spread', value: Node } // 展开成按位置传的几个:*args、...args;value 是展开的那个表达式
|
||||
| { kind: 'keywordSpread', value: Node } // 展开成按名字传的几个:Python 的 **kwargs
|
||||
|
||||
/**
|
||||
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用排成
|
||||
* 「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
|
||||
*/
|
||||
export interface CallFacts {
|
||||
/** 被调用的表达式:`user_service.update_user`、`UserDetailResponse`;TypeScript 的 new 不算在里面。 */
|
||||
callee: Node
|
||||
/** callee 里最后那个名字(点分名字的最后一节):着色、查定义用它;callee 不是名字(`f()()`、`handlers[k]()`)时没有。 */
|
||||
name?: Node
|
||||
/**
|
||||
* 写法本身说明了是不是新建对象时给:TypeScript 的 `new X()` 是,不带 new 的调用不是。
|
||||
* Python 新建对象和调用函数写法一样,不给,翻译插件问 name 的定义是哪种块(SourceDocument.calleeKind)。
|
||||
*/
|
||||
construct?: boolean
|
||||
/** 括号连同里面的实参:它跨了几行,这次调用就是跨几行写的。 */
|
||||
argumentList: Node
|
||||
arguments: CallArgument[]
|
||||
}
|
||||
|
||||
export interface DeclaratorFacts {
|
||||
/** 变量名;解构声明是整个模式。 */
|
||||
name: Node
|
||||
@@ -331,6 +357,18 @@ export interface PropertyRow extends Defines {
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
|
||||
export interface PreviewTable {
|
||||
columns: string[]
|
||||
items: PropertyItem[]
|
||||
}
|
||||
|
||||
/** 一行译文的块翻出来的:一行译文(注释块是 Markdown 说明);跨几行写的调用还有摊开各个实参的表,预览排在这一行下面。 */
|
||||
export interface TextTranslation {
|
||||
text: PreviewText
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。
|
||||
* 翻译插件翻一块时从 SourceDocument.language 拿到解析这份源码的语言插件,不直接依赖某个语言插件。
|
||||
@@ -347,6 +385,8 @@ export interface LanguageAdapter {
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
|
||||
call?: (node: Node) => CallFacts | undefined
|
||||
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
|
||||
displayNames?: DisplayNameReader
|
||||
}
|
||||
@@ -384,6 +424,11 @@ export interface SourceDocument {
|
||||
language: LanguageAdapter
|
||||
/** 写在注释里给代码阅读看的记号(中文名记号,见 DisplayNameFacts.markers):不是说明文字,翻注释、说明时去掉 */
|
||||
markers: SourceRange[]
|
||||
/**
|
||||
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
|
||||
* 排视图数据是同步的,这里只交出已经查到的;还没查到、查不到、宿主不查(没给)都是 undefined,查到了宿主重排、重发。
|
||||
*/
|
||||
calleeKind?: (call: CallFacts) => BlockKind | undefined
|
||||
}
|
||||
|
||||
/** 筛选属性和值的显示名,筛选栏、块的种类(图标的悬停提示)用它;没给名字的显示 id。 */
|
||||
@@ -401,8 +446,8 @@ export interface Translator {
|
||||
languageIds: string[]
|
||||
/** 译文用的语言(BCP 47 的语言部分,例如 'zh'):库规则按它挑自己的词表。 */
|
||||
displayLanguage: string
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, document: SourceDocument) => PreviewText
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文,跨几行写的调用连着摊开实参的表。 */
|
||||
translate: (block: TextBlock, document: SourceDocument) => TextTranslation
|
||||
/** 函数、类、枚举、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
|
||||
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { RETRY_DELAYS } from './nameResolution'
|
||||
|
||||
/** 排一遍视图数据用的:同步地问(交出已经查到的),和这一遍问了哪些问题。 */
|
||||
export interface AnswerSession<Q, A> {
|
||||
ask: (question: Q) => A | undefined
|
||||
questions: () => Q[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查一个问题:查到了是答案,查了没有是 null;语言扩展没答(还没起来)是 undefined,已知的留着不动,隔一会儿再查。
|
||||
*/
|
||||
export type Answer<Q, A> = (question: Q) => Promise<A | null | undefined>
|
||||
|
||||
/**
|
||||
* 一份打开着的文档里,排视图数据时要问、却要异步才查得到的问题(库规则问的名字的定义、调用的名字定义成哪种块)的答案,
|
||||
* 按问题的 id 记,id 一样的问题按一个查。这里不碰 VS Code。
|
||||
* 排视图数据是同步的:先交出已经查到的(文档改了也照旧用上一版查到的),记下这一遍问了什么;发出预览后再查,
|
||||
* 每个版本同一个问题查一次(语言扩展没答的照 RETRY_DELAYS 隔一会儿再查),查到的和已知的不一样就 changed,宿主按新结果重排、重发。
|
||||
*/
|
||||
export class DeferredAnswers<Q, A> {
|
||||
/** 查到的:查了没有是 null */
|
||||
private readonly known = new Map<string, A | null>()
|
||||
private version: number | undefined
|
||||
/** 眼下这个版本查过(或正在查)的问题 */
|
||||
private readonly checked = new Set<string>()
|
||||
private timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
constructor(
|
||||
private readonly idOf: (question: Q) => string,
|
||||
/** 两个答案对问的人来说一样不一样 */
|
||||
private readonly same: (a: A | null | undefined, b: A | null | undefined) => boolean,
|
||||
private readonly changed: () => void,
|
||||
private readonly delays: readonly number[] = RETRY_DELAYS,
|
||||
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
|
||||
) {}
|
||||
|
||||
session(): AnswerSession<Q, A> {
|
||||
const asked = new Map<string, Q>()
|
||||
return {
|
||||
ask: (question) => {
|
||||
const id = this.idOf(question)
|
||||
if (!asked.has(id))
|
||||
asked.set(id, question)
|
||||
return this.known.get(id) ?? undefined
|
||||
},
|
||||
questions: () => [...asked.values()],
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版问的问题:这个版本查过的不再查。 */
|
||||
resolve(version: number, questions: readonly Q[], answer: Answer<Q, A>): void {
|
||||
if (this.version !== version) {
|
||||
clearTimeout(this.timer)
|
||||
this.version = version
|
||||
this.checked.clear()
|
||||
}
|
||||
const fresh = questions.filter(question => !this.checked.has(this.idOf(question)))
|
||||
for (const question of fresh)
|
||||
this.checked.add(this.idOf(question))
|
||||
if (fresh.length === 0)
|
||||
return
|
||||
const attempt = (pending: readonly Q[], tries: number): void => {
|
||||
void this.check(pending, answer).then(({ unanswered, changed }) => {
|
||||
if (this.version !== version)
|
||||
return
|
||||
if (changed)
|
||||
this.changed()
|
||||
if (unanswered.length > 0 && tries < this.delays.length)
|
||||
this.timer = this.schedule(() => attempt(unanswered, tries + 1), this.delays[tries])
|
||||
})
|
||||
}
|
||||
attempt(fresh, 0)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.version = undefined
|
||||
}
|
||||
|
||||
/** 查一遍:没答的问题留着已知的不动,交回去等下一次再查。 */
|
||||
private async check(questions: readonly Q[], answer: Answer<Q, A>): Promise<{ unanswered: Q[], changed: boolean }> {
|
||||
const unanswered: Q[] = []
|
||||
let changed = false
|
||||
await Promise.all(questions.map(async (question) => {
|
||||
const found = await answer(question)
|
||||
if (found === undefined) {
|
||||
unanswered.push(question)
|
||||
return
|
||||
}
|
||||
const id = this.idOf(question)
|
||||
if (!this.same(this.known.get(id), found))
|
||||
changed = true
|
||||
this.known.set(id, found)
|
||||
}))
|
||||
return { unanswered, changed }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Location, LocationLink, Position } from 'vscode'
|
||||
import type { RuleLocation } from '../contract'
|
||||
import type { BlockKind, LanguageAdapter, RuleLocation } from '../contract'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DefinitionTarget } from './definitionAction'
|
||||
import { commands, Range, Uri, window, workspace } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { readDefinitionKind } from '../blockView'
|
||||
|
||||
/** 查到的一处定义:文件和定义处那个名字的范围。 */
|
||||
export interface FoundLocation { uri: Uri, range: Range }
|
||||
@@ -36,6 +38,23 @@ export function ruleDefinitions(locations: readonly RuleLocation[]): FoundLocati
|
||||
return locations.map(({ uri, start, end }) => ({ uri: Uri.parse(uri), range: new Range(start.line, start.character, end.line, end.character) }))
|
||||
}
|
||||
|
||||
/** 定义处那个名字定义的是哪种块:打开目标文件,用它那种语言的语言插件分块认;目标打不开、语言不支持、不在哪块的名字上时没有。 */
|
||||
export async function definitionKind(
|
||||
location: FoundLocation,
|
||||
languageFor: (languageId: string) => LanguageAdapter | undefined,
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
): Promise<BlockKind | undefined> {
|
||||
try {
|
||||
const document = await workspace.openTextDocument(location.uri)
|
||||
const adapter = languageFor(document.languageId)
|
||||
return adapter && await readDefinitionKind(runtime, adapter, document.languageId, document.getText(), document.offsetAt(location.range.start))
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `认不出 ${location.uri.toString()} 里定义的是哪种块:${err instanceof Error ? err.message : String(err)}` })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 查到的各处定义打开目标文件补上行文字、下标;目标打不开成文本的(例如 Python 命名空间包指向的目录)略过。 */
|
||||
export async function findDefinitions(locations: readonly FoundLocation[], supports: (languageId: string) => boolean): Promise<Definition[]> {
|
||||
const found = await Promise.all(locations.map(async (location): Promise<Definition[]> => {
|
||||
|
||||
@@ -118,6 +118,21 @@ describe('预览的 document 消息', () => {
|
||||
expect(reports).toEqual(['库规则 fastapi 给筛选属性起名时出错,筛选栏照写 id:词表坏了'])
|
||||
})
|
||||
|
||||
it('调用的名字定义成哪种块交给 calleeKind 问:跨几行新建对象的一行译文下面带实参表', async () => {
|
||||
const source = 'def f(user):\n return Detail(\n id=user.id,\n name=user.name,\n )\n'
|
||||
const asked: string[] = []
|
||||
const { message } = await documentMessage(doc('python', source), registryWithPython(), runtime, ignore, undefined, (call) => {
|
||||
asked.push(call.callee.text)
|
||||
return 'class'
|
||||
})
|
||||
expect(asked).toEqual(['Detail'])
|
||||
expect(message.blocks[0].children[0]).toMatchObject({
|
||||
kind: 'return',
|
||||
translation: [{ text: '返回' }, { text: ' ' }, { text: '新建' }, { text: ' ' }, { text: 'Detail', role: 'type' }, { text: ' ' }, { text: '· 2 个参数', role: 'hint' }],
|
||||
table: { columns: ['参数', '值'], items: [{ cells: [[{ text: 'id' }], expect.anything()] }, { cells: [[{ text: 'name' }], expect.anything()] }] },
|
||||
})
|
||||
})
|
||||
|
||||
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
|
||||
const broken: LibraryRule = {
|
||||
id: 'broken',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DisplayName, RuleContext } from '../contract'
|
||||
import type { DisplayName, RuleContext, SourceDocument } from '../contract'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentMessage } from './messages'
|
||||
@@ -21,7 +21,8 @@ export interface DocumentRead {
|
||||
|
||||
/**
|
||||
* 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下;
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup)。筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup),翻译插件问调用的名字定义成哪种块时问 calleeKind(SourceDocument.calleeKind)。
|
||||
* 筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
|
||||
*/
|
||||
export async function documentMessage(
|
||||
document: DocumentSnapshot,
|
||||
@@ -29,6 +30,7 @@ export async function documentMessage(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
report: (message: string) => void,
|
||||
lookup?: RuleContext['lookup'],
|
||||
calleeKind?: SourceDocument['calleeKind'],
|
||||
): Promise<DocumentRead> {
|
||||
const { languageId, version } = document
|
||||
const source = document.getText()
|
||||
@@ -48,7 +50,7 @@ export async function documentMessage(
|
||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
|
||||
try {
|
||||
const rules = registry.rulesFor(languageId)
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup })
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup }, calleeKind)
|
||||
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
|
||||
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames }
|
||||
}
|
||||
|
||||
@@ -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 { RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
|
||||
import type { BlockKind, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
|
||||
import type { DefinitionStart, IndexedName, NameSources } from '../nameResolution'
|
||||
import type { CodeQa } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
@@ -14,13 +14,14 @@ import type { ExtensionToWebview, 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 { DocumentNames, resolveNames } from '../nameResolution'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
@@ -131,6 +132,8 @@ class Preview {
|
||||
private readonly names: DocumentNames
|
||||
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
|
||||
private readonly lookups: DocumentLookups
|
||||
/** 调用的名字定义成哪种块(分新建还是调用):查到的变了就重发文档 */
|
||||
private readonly callees: DocumentCallees
|
||||
|
||||
constructor(
|
||||
private readonly document: TextDocument,
|
||||
@@ -154,6 +157,7 @@ class Preview {
|
||||
this.post({ type: 'displayNames', version, names })
|
||||
})
|
||||
this.lookups = new DocumentLookups(() => this.schedulePush())
|
||||
this.callees = new DocumentCallees(() => this.schedulePush())
|
||||
this.disposables = [
|
||||
panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
|
||||
panel.onDidChangeViewState(() => {
|
||||
@@ -223,9 +227,10 @@ class Preview {
|
||||
const { languageId, version } = this.document
|
||||
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) => {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||
}, session.lookup)
|
||||
}, session.lookup, callees.calleeKind)
|
||||
if (request !== this.latest || !this.ready)
|
||||
return
|
||||
this.definitions.remember(message.version, message.blocks)
|
||||
@@ -237,9 +242,24 @@ class Preview {
|
||||
if (version === this.document.version) {
|
||||
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report))
|
||||
this.lookups.resolve(version, session.requests(), this.lookupSources())
|
||||
this.callees.resolve(version, callees.requests(), offset => this.calleeKind(offset))
|
||||
}
|
||||
}
|
||||
|
||||
/** 这份文档里 offset 处名字的定义是哪种块:语言扩展查到的几处定义里第一处认得出的;语言扩展没答是 undefined。 */
|
||||
private async calleeKind(offset: number): Promise<BlockKind | null | undefined> {
|
||||
const { document } = this
|
||||
const found = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (found.length === 0)
|
||||
return undefined
|
||||
for (const location of found) {
|
||||
const kind = await definitionKind(location, languageId => this.registry.languageFor(languageId), this.runtime)
|
||||
if (kind)
|
||||
return kind
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 这份文档里 offset 处名字的定义(语言扩展查的)落在哪儿。 */
|
||||
private async definitionStarts(offset: number): Promise<DefinitionStart[]> {
|
||||
const { document } = this
|
||||
@@ -284,6 +304,7 @@ class Preview {
|
||||
clearTimeout(this.timer)
|
||||
this.names.dispose()
|
||||
this.lookups.dispose()
|
||||
this.callees.dispose()
|
||||
for (const disposable of this.disposables)
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { window } from 'vscode'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
|
||||
export async function removeWorktree(panel: Panel, relPath: string, issueNumber: number): Promise<void> {
|
||||
const state = await panel.readIssueState(issueNumber)
|
||||
const branch = typeof state.branch === 'string' ? state.branch : ''
|
||||
const choice = await window.showWarningMessage(
|
||||
branch ? `确认删除 worktree ${relPath} 及其分支 ${branch}?` : `确认删除 worktree ${relPath}?`,
|
||||
{ modal: true },
|
||||
'删除',
|
||||
)
|
||||
if (choice !== '删除')
|
||||
return
|
||||
}
|
||||
|
||||
class Preview {
|
||||
constructor(document: TextDocument, owner: Owner) {
|
||||
this.definitions = new DefinitionSources({
|
||||
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
|
||||
language: offset => languageDefinitions(document.uri, document.positionAt(offset)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function commentsUrl(opts: Options): URL {
|
||||
const url = new URL(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/comments`,
|
||||
)
|
||||
return url
|
||||
}
|
||||
|
||||
export function merged(base: string[], extra: string[]): string[] {
|
||||
return concatAll(
|
||||
base,
|
||||
...extra,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IterationFacts, Node } from '../contract'
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { factsByLine, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { factsByLine, nodeFacts, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createPythonAdapter } from './python'
|
||||
|
||||
const adapter = createPythonAdapter(WASM_DIR)
|
||||
@@ -488,3 +488,28 @@ describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
expect(await iterations('x = [1, 2]\ny = {"a": 1}\nz = (a, b)\nfor x in y:\n pass')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 调用', () => {
|
||||
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.call?.(node))
|
||||
|
||||
it('实参照源码顺序:按位置、按名字、*x、**x;新建对象和调用写法一样,不给 construct;点分名字取最后一节', async () => {
|
||||
const source = 'user_service.update_user(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)'
|
||||
expect(await calls(source)).toEqual([{
|
||||
callee: 'user_service.update_user',
|
||||
name: 'update_user',
|
||||
argumentList: '(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)',
|
||||
arguments: [
|
||||
{ kind: 'positional', value: 'db' },
|
||||
{ kind: 'keyword', name: 'user_id', value: 'user_id' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{ kind: 'keywordSpread', value: 'options' },
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
it('唯一的实参是生成器表达式时就是它;被调用的不是名字时没有 name;装饰器、父类列表不是调用', async () => {
|
||||
expect(await calls('sum(x for x in xs)')).toEqual([{ callee: 'sum', name: 'sum', argumentList: '(x for x in xs)', arguments: [{ kind: 'positional', value: '(x for x in xs)' }] }])
|
||||
expect(await calls('handlers[k]()')).toEqual([{ callee: 'handlers[k]', argumentList: '()', arguments: [] }])
|
||||
expect(await calls('@cache\nclass A(Base):\n pass')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { pythonNameReader } from './pythonNames'
|
||||
@@ -363,6 +363,42 @@ function pythonIteration(node: Node): IterationFacts | undefined {
|
||||
}
|
||||
|
||||
/** elif / else 展平成紧跟 if 的兄弟块。 */
|
||||
/** 调用的实参:name=value 按名字传,*x、**x 展开,别的按位置传;唯一的实参是生成器表达式(f(x for x in y))时就是它。 */
|
||||
function callArgument(arg: Node): CallArgument | undefined {
|
||||
switch (arg.type) {
|
||||
case 'keyword_argument': {
|
||||
const name = arg.childForFieldName('name')
|
||||
const value = arg.childForFieldName('value')
|
||||
return name && value ? { kind: 'keyword', name, value } : undefined
|
||||
}
|
||||
case 'list_splat':
|
||||
case 'dictionary_splat': {
|
||||
const [value] = syntaxChildren(arg)
|
||||
if (!value)
|
||||
return undefined
|
||||
return { kind: arg.type === 'list_splat' ? 'spread' : 'keywordSpread', value }
|
||||
}
|
||||
default:
|
||||
return { kind: 'positional', value: arg }
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用的结构:新建对象和调用函数写法一样,不给 construct。被调用的是名字或点分名字时 name 是最后那个名字。 */
|
||||
function pythonCall(node: Node): CallFacts | undefined {
|
||||
const callee = node.type === 'call' ? node.childForFieldName('function') : null
|
||||
const list = node.childForFieldName('arguments')
|
||||
if (!callee || !list)
|
||||
return undefined
|
||||
const name = callee.type === 'identifier' ? callee : callee.type === 'attribute' ? callee.childForFieldName('attribute') : null
|
||||
const args = list.type === 'generator_expression' ? [list] : syntaxChildren(list)
|
||||
return {
|
||||
callee,
|
||||
...(name && { name }),
|
||||
argumentList: list,
|
||||
arguments: args.map(callArgument).filter(arg => arg !== undefined),
|
||||
}
|
||||
}
|
||||
|
||||
function ifBlocks(node: Node): CodeBlock[] {
|
||||
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
|
||||
for (const alternative of node.childrenForFieldName('alternative')) {
|
||||
@@ -603,6 +639,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
|
||||
tokenRole: pythonTokenRole,
|
||||
iteration: pythonIteration,
|
||||
call: pythonCall,
|
||||
displayNames: pythonNameReader,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { factsByLine, nodeFacts, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createTypeScriptAdapter } from './typescript'
|
||||
|
||||
const adapter = createTypeScriptAdapter(WASM_DIR)
|
||||
@@ -612,3 +612,23 @@ describe('typeScript 枚举', () => {
|
||||
expect(byLine[1]).toMatchObject({ members: [{ name: 'A', docs: [] }, { name: 'B', docs: ['/** 第二 */'] }, { name: 'C', docs: ['// 第三'] }] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 调用', () => {
|
||||
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.call?.(node))
|
||||
|
||||
it('new X(…) 是新建,被调用的不含 new;别的调用不是新建;...x 是展开;点分名字取最后一节', async () => {
|
||||
expect(await calls('new vscode.Uri(a, ...rest)')).toEqual([{
|
||||
callee: 'vscode.Uri',
|
||||
name: 'Uri',
|
||||
construct: true,
|
||||
argumentList: '(a, ...rest)',
|
||||
arguments: [{ kind: 'positional', value: 'a' }, { kind: 'spread', value: 'rest' }],
|
||||
}])
|
||||
expect(await calls('run<T>({ a: 1 })')).toEqual([{ callee: 'run', name: 'run', construct: false, argumentList: '({ a: 1 })', arguments: [{ kind: 'positional', value: '{ a: 1 }' }] }])
|
||||
})
|
||||
|
||||
it('带标签的模板字符串、不带括号的 new X 不是调用', async () => {
|
||||
expect(await calls('sql`select 1`')).toEqual([])
|
||||
expect(await calls('new Date')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
@@ -542,6 +542,26 @@ const LITERAL_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this',
|
||||
const STRING_NODES = new Set(['string', 'template_string', 'regex'])
|
||||
const FUNCTION_NAMED = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'function', 'generator_function', 'method_definition', 'method_signature', 'abstract_method_signature', 'function_signature'])
|
||||
|
||||
/** 调用的结构:`new X(…)` 是新建对象,别的调用不是;带标签的模板字符串(sql`…`)、不带括号的 `new X` 不算。 */
|
||||
function typescriptCall(node: Node): CallFacts | undefined {
|
||||
const construct = node.type === 'new_expression'
|
||||
const callee = construct ? node.childForFieldName('constructor') : node.type === 'call_expression' ? node.childForFieldName('function') : null
|
||||
const list = node.childForFieldName('arguments')
|
||||
if (!callee || list?.type !== 'arguments')
|
||||
return undefined
|
||||
const name = callee.type === 'identifier' ? callee : callee.type === 'member_expression' ? callee.childForFieldName('property') : null
|
||||
return {
|
||||
callee,
|
||||
...(name && { name }),
|
||||
construct,
|
||||
argumentList: list,
|
||||
arguments: syntaxChildren(list).map((arg): CallArgument => {
|
||||
const spread = arg.type === 'spread_element' ? syntaxChildren(arg)[0] : undefined
|
||||
return spread ? { kind: 'spread', value: spread } : { kind: 'positional', value: arg }
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function insideString(leaf: Node): boolean {
|
||||
for (let node = leaf.parent; node; node = node.parent) {
|
||||
if (node.type === 'template_substitution')
|
||||
@@ -620,6 +640,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
},
|
||||
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
|
||||
tokenRole: typescriptTokenRole,
|
||||
call: typescriptCall,
|
||||
displayNames: typescriptNameReader,
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,13 @@ describe('中文名落到哪些名字上', () => {
|
||||
expect(rendered.get(at(ROUTES, 'delete_item'))).toBe('delete_item')
|
||||
})
|
||||
|
||||
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)里带下标的名字也算', async () => {
|
||||
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
|
||||
const rendered = renderedNames((await read(source)).blocks)
|
||||
expect(rendered.get(at(source, 'id='))).toBe('id')
|
||||
expect(rendered.get(at(source, 'user.id'))).toBe('user')
|
||||
})
|
||||
|
||||
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
|
||||
const { blocks, displayNames } = await read(ROUTES)
|
||||
const definition = at(ROUTES, 'current_user')
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ShownName {
|
||||
definition?: true
|
||||
}
|
||||
|
||||
/** 视图数据里照抄自源码、带下标的名字(一行译文、属性表的值、各层子行的格子、框里):下标 → 原文。 */
|
||||
/** 视图数据里照抄自源码、带下标的名字(一行译文和它下面的表、属性表的值、各层子行的格子、框里):下标 → 原文。 */
|
||||
export function renderedNames(blocks: readonly BlockView[]): Map<number, string> {
|
||||
const found = new Map<number, string>()
|
||||
const inline = (pieces: readonly Inline[]): void => {
|
||||
@@ -46,6 +46,7 @@ export function renderedNames(blocks: readonly BlockView[]): Map<number, string>
|
||||
}
|
||||
else {
|
||||
text(block.translation)
|
||||
items(block.table?.items)
|
||||
}
|
||||
visit(block.children)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ function adapter(id: string, languageIds: string[]): LanguageAdapter {
|
||||
}
|
||||
|
||||
function translator(id: string, languageIds: string[]): Translator {
|
||||
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => [], describe: () => [], expression: () => [], doc: () => undefined }
|
||||
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => ({ text: [] }), describe: () => [], expression: () => [], doc: () => undefined }
|
||||
}
|
||||
|
||||
function rule(id: string, languageIds: string[]): LibraryRule {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IndexEntry, RuleContext, WorkspaceIndex } from './contract'
|
||||
import type { DefinitionStart } from './nameResolution'
|
||||
import type { IndexedEntry } from './workspaceIndex'
|
||||
import { RETRY_DELAYS } from './nameResolution'
|
||||
import { DeferredAnswers } from './deferredAnswers'
|
||||
|
||||
/**
|
||||
* 库规则认领时查的名字(RuleContext.lookup):名字的定义在规则的工作区索引里是哪一项。
|
||||
@@ -33,91 +33,44 @@ function same(a: IndexEntry | null | undefined, b: IndexEntry | null | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 一份打开着的文档里库规则问的名字查到了什么:按「索引 id + 名字原文」记,同一份文档里同名的名字按一处查。
|
||||
* 排视图数据是同步的:先交出已经查到的(文档改了也照旧用上一版查到的),记下这一遍问了什么;发出预览后再查,
|
||||
* 每个版本同一个名字查一次(索引里有同名的项、语言扩展却没答时照 RETRY_DELAYS 隔一会儿再查),查到的和已知的不一样就 changed,
|
||||
* 宿主按新结果重排、重发。
|
||||
* 一份打开着的文档里库规则问的名字查到了什么:按「索引 id + 名字原文」记,同一份文档里同名的名字按一处查;
|
||||
* 什么时候查、查到了怎么叫宿主重排见 DeferredAnswers。
|
||||
*/
|
||||
export class DocumentLookups {
|
||||
/** 查到的:没有这个定义是 null */
|
||||
private readonly known = new Map<string, IndexEntry | null>()
|
||||
private version: number | undefined
|
||||
/** 眼下这个版本查过(或正在查)的名字 */
|
||||
private readonly checked = new Set<string>()
|
||||
private timer: ReturnType<typeof setTimeout> | undefined
|
||||
private readonly answers: DeferredAnswers<LookupRequest, IndexEntry>
|
||||
|
||||
constructor(
|
||||
private readonly changed: () => void,
|
||||
private readonly delays: readonly number[] = RETRY_DELAYS,
|
||||
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
|
||||
) {}
|
||||
constructor(changed: () => void, delays?: readonly number[], schedule?: (run: () => void, ms: number) => ReturnType<typeof setTimeout>) {
|
||||
this.answers = new DeferredAnswers(({ index, key }) => idOf(index, key), same, changed, delays, schedule)
|
||||
}
|
||||
|
||||
session(): LookupSession {
|
||||
const requests = new Map<string, LookupRequest>()
|
||||
const session = this.answers.session()
|
||||
return {
|
||||
lookup: (index, name) => {
|
||||
const id = idOf(index, name.text)
|
||||
if (!requests.has(id))
|
||||
requests.set(id, { index, key: name.text, offset: name.startIndex })
|
||||
return this.known.get(id) ?? undefined
|
||||
},
|
||||
requests: () => [...requests.values()],
|
||||
lookup: (index, name) => session.ask({ index, key: name.text, offset: name.startIndex }),
|
||||
requests: session.questions,
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版规则问的名字:这个版本查过的不再查。 */
|
||||
resolve(version: number, requests: readonly LookupRequest[], sources: LookupSources): void {
|
||||
if (this.version !== version) {
|
||||
clearTimeout(this.timer)
|
||||
this.version = version
|
||||
this.checked.clear()
|
||||
}
|
||||
const fresh = requests.filter(request => !this.checked.has(idOf(request.index, request.key)))
|
||||
for (const request of fresh)
|
||||
this.checked.add(idOf(request.index, request.key))
|
||||
if (fresh.length === 0)
|
||||
return
|
||||
const attempt = (tries: number): void => {
|
||||
void this.check(fresh, sources).then(({ settled, changed }) => {
|
||||
if (this.version !== version)
|
||||
return
|
||||
if (changed)
|
||||
this.changed()
|
||||
if (!settled && tries < this.delays.length)
|
||||
this.timer = this.schedule(() => attempt(tries + 1), this.delays[tries])
|
||||
})
|
||||
}
|
||||
attempt(0)
|
||||
this.answers.resolve(version, requests, request => find(request, sources))
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.version = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 查一遍:索引里没有同名的项就是没有这个定义;有就问语言扩展,定义处正是其中一项才算。
|
||||
* 语言扩展没答(还没起来)的名字留着已知的不动,算没查完(settled 为 false)。
|
||||
*/
|
||||
private async check(requests: readonly LookupRequest[], sources: LookupSources): Promise<{ settled: boolean, changed: boolean }> {
|
||||
let settled = true
|
||||
let changed = false
|
||||
await Promise.all(requests.map(async ({ index, key, offset }) => {
|
||||
const id = idOf(index, key)
|
||||
const candidates = await sources.find(index, key)
|
||||
let found: IndexEntry | null = null
|
||||
if (candidates.length > 0) {
|
||||
const definitions = await sources.definitions(offset)
|
||||
if (definitions.length === 0) {
|
||||
settled = false
|
||||
return
|
||||
}
|
||||
found = candidates.find(({ location }) => definitions.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))?.entry ?? null
|
||||
}
|
||||
if (!same(this.known.get(id), found))
|
||||
changed = true
|
||||
this.known.set(id, found)
|
||||
}))
|
||||
return { settled, changed }
|
||||
this.answers.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查规则问的一个名字:索引里没有同名的项就是没有这个定义;有就问语言扩展,定义处正是其中一项才算。
|
||||
* 语言扩展没答(还没起来)是 undefined。
|
||||
*/
|
||||
async function find({ index, key, offset }: LookupRequest, sources: LookupSources): Promise<IndexEntry | null | undefined> {
|
||||
const candidates = await sources.find(index, key)
|
||||
if (candidates.length === 0)
|
||||
return null
|
||||
const definitions = await sources.definitions(offset)
|
||||
if (definitions.length === 0)
|
||||
return undefined
|
||||
return candidates.find(({ location }) => definitions.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))?.entry ?? null
|
||||
}
|
||||
@@ -82,6 +82,28 @@ export function nodesToText(value: unknown): unknown {
|
||||
return value
|
||||
}
|
||||
|
||||
/** 源码里 read 认得出的每个节点(先序,外层在前)读出的结构,节点换成源码。 */
|
||||
export async function nodeFacts(adapter: LanguageAdapter, languageId: string, source: string, read: (node: Node) => unknown): Promise<unknown[]> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
const found: unknown[] = []
|
||||
const visit = (node: Node): void => {
|
||||
const facts = read(node)
|
||||
if (facts)
|
||||
found.push(nodesToText(facts))
|
||||
for (const child of node.children) {
|
||||
if (child)
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(tree.rootNode)
|
||||
return found
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** 每个属性表块的 facts(节点换成源码),按 header 起始行(1 起)索引。 */
|
||||
export function factsByLine(blocks: CodeBlock[]): Record<number, unknown> {
|
||||
const byLine: Record<number, unknown> = {}
|
||||
|
||||
@@ -53,8 +53,8 @@ const translator = createZhTemplateTranslator({
|
||||
|
||||
function translateFirst(language: LanguageAdapter, source: string): Promise<string> {
|
||||
return withBlocks(language, 'fake', source, ([block]) => {
|
||||
const translation = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in translation ? translation.markdown : segmentText(translation)
|
||||
const { text } = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Inline, LanguageAdapter, PropertyItem, PropertyRow, Segment, TokenRole, Translator } from '../contract'
|
||||
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
|
||||
import type { ZhTable } from './zhTemplate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
@@ -11,34 +11,37 @@ import { createZhTemplateTranslator } from './zhTemplate'
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Inline[], markdown?: string }
|
||||
/** 一块的译文:一行译文在 segments 里,下面摊开实参的表在 table 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Inline[], markdown?: string, table?: PreviewTable }
|
||||
|
||||
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||
/**
|
||||
* 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。
|
||||
* calleeKind 同 SourceDocument.calleeKind(调用的名字定义成哪种块),不给就是宿主不查。
|
||||
*/
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, Translated>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, Translated>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (byLine.has(line) || 'facts' in block)
|
||||
return
|
||||
const translation = translator.translate(block, { source, language: adapter, markers: [] })
|
||||
const { text: translation, table } = translator.translate(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) })
|
||||
byLine.set(line, 'markdown' in translation
|
||||
? { kind: block.kind, segments: [], markdown: translation.markdown }
|
||||
: { kind: block.kind, segments: translation })
|
||||
: { kind: block.kind, segments: translation, ...(table && { table }) })
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
}
|
||||
|
||||
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引。 */
|
||||
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, PropertyRow[]>> {
|
||||
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引;calleeKind 同 translateAll。 */
|
||||
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, PropertyRow[]>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, PropertyRow[]>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line) && 'facts' in block)
|
||||
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [] }))
|
||||
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) }))
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
@@ -165,12 +168,12 @@ describe('typeScript 中文直译', () => {
|
||||
|
||||
it('多行合成一行、去掉右括号前的尾逗号、行中注释不进译文', async () => {
|
||||
const source = [
|
||||
'call(',
|
||||
'list = [',
|
||||
' first, // 第一个',
|
||||
' [],',
|
||||
')',
|
||||
']',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('call( first, 空数组 )')
|
||||
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('list = [ first, 空数组 ]')
|
||||
})
|
||||
|
||||
it('as / in / extends / default 按所在语法节点换译法', async () => {
|
||||
@@ -417,8 +420,10 @@ describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
5: '⟦集合|遍历 pairs 的每个 n, _,取 n⟧',
|
||||
6: '⟦列表|遍历 grid 的每个 row,取 ⟦列表|遍历 row 的每个 y,取 y⟧⟧',
|
||||
7: 'pick(a, ⟦生成器|遍历 b 的每个 y,取 y⟧)',
|
||||
8: 'sum(⟦生成器|遍历 counts 的每个 n,取 n⟧)',
|
||||
8: '调用 sum · 1 个参数',
|
||||
})
|
||||
// 跨几行写的调用唯一的实参是生成器:共用的那对括号属于调用,实参表里只有框
|
||||
expect(propertyLines(map.get(8) ?? []).at(-1)).toBe(' 第 1 个 | ⟦生成器|遍历 counts 的每个 n,取 n⟧')
|
||||
const async = valueOf(map.get(3), '初始值')[0]
|
||||
expect('segments' in async && async.segments.filter(piece => 'role' in piece && piece.role === 'keyword')).toEqual([
|
||||
{ text: '异步遍历', role: 'keyword' },
|
||||
@@ -1028,6 +1033,173 @@ describe('源码下标(跳到定义用)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('跨几行写的调用', () => {
|
||||
const routes = readFixture('routes.py')
|
||||
const calls = readFixture('calls.ts.txt')
|
||||
|
||||
/** 调用的名字定义成哪种块(按被调用的表达式原文查,没有的是还没查到),并记下问了哪些。 */
|
||||
function kinds(known: Record<string, BlockKind>): { asked: string[], calleeKind: (call: CallFacts) => BlockKind | undefined } {
|
||||
const asked: string[] = []
|
||||
return {
|
||||
asked,
|
||||
calleeKind: (call) => {
|
||||
asked.push(call.callee.text)
|
||||
return known[call.callee.text]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function tableLines(table: PreviewTable | undefined): string[] {
|
||||
return (table?.items ?? []).map(item => item.cells.map(segmentText).join(' | '))
|
||||
}
|
||||
|
||||
const USER_DETAIL = [
|
||||
'id | user.id',
|
||||
'username | user.username',
|
||||
'display_name | user.display_name',
|
||||
'role | user.role.value',
|
||||
'email | user.email',
|
||||
'phone | user.phone',
|
||||
'bio | user.bio',
|
||||
'avatar_url | user.avatar_url',
|
||||
'is_active | user.is_active',
|
||||
'created_at | user.created_at',
|
||||
'updated_at | user.updated_at',
|
||||
]
|
||||
|
||||
it('返回里新建对象(名字定义成类):一行「返回 新建 UserDetailResponse · 11 个参数」,实参摊成表(参数 | 值),按名字传的写名字', async () => {
|
||||
const { asked, calleeKind } = kinds({ UserDetailResponse: 'class' })
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
|
||||
expect(colors(line?.segments)).toEqual([
|
||||
{ text: '返回', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '新建', role: 'storage' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'UserDetailResponse', role: 'type' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '· 11 个参数', role: 'hint' },
|
||||
])
|
||||
expect(line?.table?.columns).toEqual(['参数', '值'])
|
||||
expect(tableLines(line?.table)).toEqual(USER_DETAIL)
|
||||
expect(asked).toContain('UserDetailResponse')
|
||||
// 被调用的名字、按名字传的名字、值里的名字都带源码下标:能跳到定义、显示中文名
|
||||
const call = routes.indexOf('UserDetailResponse(')
|
||||
expect(line?.segments.find(piece => 'role' in piece && piece.role === 'type')).toMatchObject({ offset: call })
|
||||
expect(line?.table?.items[0].cells).toEqual([
|
||||
[{ text: 'id', role: 'variable', offset: routes.indexOf('id=user.id', call) }],
|
||||
[
|
||||
{ text: 'user', role: 'variable', offset: routes.indexOf('user.id', call) },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'id', role: 'variable', offset: routes.indexOf('user.id', call) + 5 },
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('名字的定义还没查到、查不到、是函数都按调用算:被调用的名字按函数着色', async () => {
|
||||
for (const calleeKind of [undefined, kinds({}).calleeKind, kinds({ UserDetailResponse: 'function' }).calleeKind]) {
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
|
||||
expect(text(line?.segments)).toBe('返回 调用 UserDetailResponse · 11 个参数')
|
||||
expect(line?.segments.find(piece => 'role' in piece && piece.text === 'UserDetailResponse')).toMatchObject({ role: 'function' })
|
||||
}
|
||||
const exception = (await translateAll(py, 'python', pythonZhTranslator, routes, kinds({ UserDetailResponse: 'exception' }).calleeKind)).get(96)
|
||||
expect(text(exception?.segments)).toBe('返回 新建 UserDetailResponse · 11 个参数')
|
||||
})
|
||||
|
||||
it('变量的初始值里跨几行调用:初始值一行「等待 调用 user_service.update_user · 10 个参数」,实参表展开在这一行下面,按位置传的写「第 N 个」', async () => {
|
||||
const rows = (await describeAll(py, 'python', pythonZhTranslator, routes, kinds({}).calleeKind)).get(83) ?? []
|
||||
expect(propertyLines(rows)).toEqual([
|
||||
'名称: user',
|
||||
'初始值: 等待 调用 user_service.update_user · 10 个参数',
|
||||
' 第 1 个 | db',
|
||||
' user_id | user_id',
|
||||
' display_name | body.display_name',
|
||||
' email | body.email',
|
||||
' phone | body.phone',
|
||||
' bio | body.bio',
|
||||
' avatar_url | body.avatar_url',
|
||||
' role | body.role',
|
||||
' is_active | body.is_active',
|
||||
' password | body.password',
|
||||
])
|
||||
const value = rows.find(row => row.label === '初始值')
|
||||
expect(value?.columns).toEqual(['参数', '值'])
|
||||
expect(colors(value?.value)).toEqual([
|
||||
{ text: '等待', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '调用', role: 'storage' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'user_service', role: 'variable' },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'update_user', role: 'function' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '· 10 个参数', role: 'hint' },
|
||||
])
|
||||
expect(value?.items?.[0].cells[0]).toEqual([{ text: '第 1 个', role: 'hint' }])
|
||||
})
|
||||
|
||||
it('一行写完的调用照旧一行译文,不问定义', async () => {
|
||||
const { asked, calleeKind } = kinds({})
|
||||
const map = await describeAll(py, 'python', pythonZhTranslator, routes, calleeKind)
|
||||
expect(propertyLines(map.get(43) ?? [])).toEqual(['名称: user', '初始值: 等待 user_service.get_user(db, user_id=user_id)'])
|
||||
expect(propertyLines(map.get(118) ?? [])).toEqual(['名称: payload', '初始值: 等待 file.read()'])
|
||||
expect(asked).not.toContain('user_service.get_user')
|
||||
expect(asked).not.toContain('file.read')
|
||||
})
|
||||
|
||||
it('*x 写「展开」、**x 写「展开关键字」,「第 N 个」只数按位置传的;实参里的调用不再排成表', async () => {
|
||||
const source = [
|
||||
'run(',
|
||||
' first,',
|
||||
' *rest,',
|
||||
' second,',
|
||||
' key=Inner(',
|
||||
' x=1,',
|
||||
' ),',
|
||||
' **options,',
|
||||
')',
|
||||
].join('\n')
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, source)).get(1)
|
||||
expect(text(line?.segments)).toBe('调用 run · 5 个参数')
|
||||
expect(tableLines(line?.table)).toEqual(['第 1 个 | first', '展开 | rest', '第 2 个 | second', 'key | Inner( x=1 )', '展开关键字 | options'])
|
||||
})
|
||||
|
||||
it('typeScript:new 写法就是新建,不带 new 的是调用,都不问定义;...x 写「展开」', async () => {
|
||||
const { asked, calleeKind } = kinds({})
|
||||
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
|
||||
expect(text(texts.get(18)?.segments)).toBe('this.definitions = 新建 DefinitionSources · 1 个参数')
|
||||
expect(tableLines(texts.get(18)?.table)).toEqual([
|
||||
'第 1 个 | { rule: 异步 reference => ruleDefinitions(等待 owner.resolveReference(reference)), language: offset => languageDefinitions(document.uri, document.positionAt(offset)) }',
|
||||
])
|
||||
expect(text(texts.get(33)?.segments)).toBe('返回 调用 concatAll · 2 个参数')
|
||||
expect(tableLines(texts.get(33)?.table)).toEqual(['第 1 个 | base', '展开 | extra'])
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
|
||||
// 实参是模板字符串:照抄
|
||||
expect(propertyLines(rows.get(7) ?? []).slice(1)).toEqual([
|
||||
'初始值: 等待 调用 window.showWarningMessage · 3 个参数',
|
||||
expect.stringMatching(/^ {2}第 1 个 \| 如果 branch 则 `确认删除 worktree [^`]+` 否则 `确认删除 worktree [^`]+`$/),
|
||||
' 第 2 个 | { modal: 真 }',
|
||||
' 第 3 个 | \'删除\'',
|
||||
])
|
||||
expect(propertyLines(rows.get(26) ?? []).slice(1)).toEqual(['初始值: 新建 URL · 1 个参数', expect.stringMatching(/^ {2}第 1 个 \| `[^`]+\/comments`$/)])
|
||||
expect(asked).toEqual([])
|
||||
})
|
||||
|
||||
it('链式调用跨了几行、实参列表各自写在一行里的不排成表;回调主体不在块头部里的调用也不排', async () => {
|
||||
const source = [
|
||||
'const ids = items',
|
||||
' .filter(a => a.ok)',
|
||||
' .map(a => a.id)',
|
||||
'describe(\'x\', () => {',
|
||||
' run()',
|
||||
'})',
|
||||
].join('\n')
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(propertyLines(rows.get(1) ?? []).at(-1)).toBe('值: items .filter(a => a.ok) .map(a => a.id)')
|
||||
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(texts.get(4)).toEqual(expect.not.objectContaining({ table: expect.anything() }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('中文模板引擎', () => {
|
||||
function testTranslator(table: Partial<ZhTable>): Translator {
|
||||
return createZhTemplateTranslator({
|
||||
@@ -1091,14 +1263,14 @@ describe('中文模板引擎', () => {
|
||||
const source = [
|
||||
'n += a if k in seen else b',
|
||||
'print(a if c else b if d else e)',
|
||||
'print((a # 注释不占序号',
|
||||
' if k == 1 else b))',
|
||||
'return (a # 注释不占序号',
|
||||
' if k == 1 else b)',
|
||||
].join('\n')
|
||||
const map = await translateAll(pythonColored('variable'), 'python', translator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
3: 'print((若 k 等于 1 则 a 不然 b))',
|
||||
3: 'return (若 k 等于 1 则 a 不然 b)',
|
||||
})
|
||||
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
.toEqual(['令', '加上', '若', '则', '不然'])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
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 { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -68,12 +68,23 @@ interface Token {
|
||||
group?: SegmentGroup
|
||||
}
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
/** 跨几行写的调用排成一行、实参摊成表(见 emitCall):只排遇到的第一个,它的表记在 table 里。 */
|
||||
interface CallSlot {
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
|
||||
* calls 是这一段里跨几行写的调用要不要排成一行、实参摊成表;没有就照常翻。
|
||||
*/
|
||||
interface Collector {
|
||||
document: SourceDocument
|
||||
/** 要翻的那个节点 */
|
||||
root: Node
|
||||
tokens: Token[]
|
||||
cursor: number
|
||||
gap: boolean
|
||||
calls?: CallSlot
|
||||
}
|
||||
|
||||
const CONTEXT_SEPARATOR = ' > '
|
||||
@@ -165,6 +176,9 @@ const KIND_ZH: Record<BlockKind, string> = {
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
|
||||
/** 被调用的名字定义成这几种块时,调用就是新建对象。 */
|
||||
const CONSTRUCTED = new Set<BlockKind | undefined>(['class', 'exception', 'enum'])
|
||||
const ARGUMENT_COLUMNS = ['参数', '值']
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
@@ -302,9 +316,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { style: 'iteration', label: ITERATION_ZH[facts.result], segments }
|
||||
}
|
||||
|
||||
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
|
||||
/**
|
||||
* 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。
|
||||
* 只翻这个遍历(它是调用的实参,排在实参表里)时外面的调用不在译文里,这对括号也不写。
|
||||
*/
|
||||
function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void {
|
||||
const shared = facts.sharedParentheses
|
||||
const alone = node.startIndex === out.root.startIndex && node.endIndex === out.root.endIndex
|
||||
const shared = alone ? undefined : facts.sharedParentheses
|
||||
const bracket = (paren: Node): Omit<Token, 'gap' | 'raw'> => ({ text: paren.text, role: out.document.language.tokenRole(paren), translated: false })
|
||||
if (shared)
|
||||
emitSource(out, shared.open, bracket(shared.open))
|
||||
@@ -318,6 +336,60 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
|
||||
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)
|
||||
return undefined
|
||||
const facts = call(node)
|
||||
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
|
||||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs);值照常翻,里面的调用不再排成表。
|
||||
*/
|
||||
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
|
||||
const value = (node: Node): Inline[] => joinTokens(tokensIn(node, span, document))
|
||||
let position = 0
|
||||
const label = (arg: CallArgument): Inline[] => {
|
||||
switch (arg.kind) {
|
||||
case 'keyword':
|
||||
return [segment(arg.name.text, 'variable', offsetOf(arg.name, document.language))]
|
||||
case 'positional':
|
||||
position++
|
||||
return [hint(`第 ${position} 个`)]
|
||||
case 'spread':
|
||||
return [hint('展开')]
|
||||
case 'keywordSpread':
|
||||
return [hint('展开关键字')]
|
||||
}
|
||||
}
|
||||
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(arg => ({ cells: [label(arg), value(arg.value)] })) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
|
||||
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
|
||||
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
|
||||
*/
|
||||
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
|
||||
calls.table = argumentTable(facts, span, out.document)
|
||||
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
|
||||
advance(out, node.startIndex)
|
||||
emit(out, { text: construct ? '新建' : '调用', role: 'storage', translated: true, raw: '' })
|
||||
out.gap = true
|
||||
out.cursor = facts.callee.startIndex
|
||||
const from = out.tokens.length
|
||||
collect(facts.callee, span, out)
|
||||
for (const token of out.tokens.slice(from)) {
|
||||
if (token.offset !== undefined && NAME_ROLES.has(token.role))
|
||||
token.role = token.offset !== facts.name?.startIndex ? 'variable' : construct ? 'type' : 'function'
|
||||
}
|
||||
out.gap = true
|
||||
emit(out, { text: `· ${facts.arguments.length} 个参数`, role: 'hint', translated: true, raw: '' })
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
@@ -331,6 +403,12 @@ 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)
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
if (parts) {
|
||||
advance(out, node.startIndex)
|
||||
@@ -362,9 +440,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
|
||||
const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
|
||||
/** 按译文顺序收集 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 }) }
|
||||
collect(node, span, out)
|
||||
return out.tokens
|
||||
}
|
||||
@@ -404,15 +482,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { markdown: docText(nodes.map(node => commentOf(node, document))) }
|
||||
}
|
||||
|
||||
function translate(block: TextBlock, document: SourceDocument): PreviewText {
|
||||
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block, document)
|
||||
const tokens = tokensIn(block.node, block.header, document)
|
||||
return { text: translateComment(block, document) }
|
||||
const calls: CallSlot = {}
|
||||
const tokens = tokensIn(block.node, block.header, document, calls)
|
||||
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 joinTokens(tokens)
|
||||
return { text: joinTokens(tokens), ...(calls.table && { table: calls.table }) }
|
||||
}
|
||||
|
||||
function expression(node: Node, document: SourceDocument): Inline[] {
|
||||
@@ -428,13 +507,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const { header } = block
|
||||
const { language } = document
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => {
|
||||
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
return joinTokens(tokensIn(node, span, document))
|
||||
}
|
||||
const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document))
|
||||
const phrases: Phrases = {
|
||||
translated: node => within(node, nodeSpan(node)),
|
||||
within,
|
||||
expanded: (node) => {
|
||||
const calls: CallSlot = {}
|
||||
const text = joinTokens(tokensIn(node, clipped(nodeSpan(node)), document, calls))
|
||||
return { text, ...(calls.table && { table: calls.table }) }
|
||||
},
|
||||
entryCount: (literal, size) => {
|
||||
const name = table.mapLiterals[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
@@ -533,6 +615,8 @@ interface Phrases {
|
||||
translated: (node: Node) => Inline[]
|
||||
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
|
||||
within: (node: Node, span: SourceSpan) => Inline[]
|
||||
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
|
||||
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
|
||||
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
|
||||
entryCount: (literal: Node, size: number) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
@@ -760,7 +844,7 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,字典 / 对象字面量见 literalValue);
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,见 declaredValue);
|
||||
* 说明(有才显示)是整条语句的,排在第一个名称后面。
|
||||
*/
|
||||
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
@@ -770,10 +854,21 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
...(i === 0 ? docRows(block.facts.doc, say) : []),
|
||||
...accessRows(block.facts.access),
|
||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||
{ label: valueLabel, ...(declarator.value ? literalValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,
|
||||
* 实参摊成表(参数 | 值)展开在这一行下面。
|
||||
*/
|
||||
function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
|
||||
if (entries?.length)
|
||||
return literalValue(value, entries, say)
|
||||
const { text, table } = say.expanded(value)
|
||||
return table ? { value: text, ...table } : { value: text }
|
||||
}
|
||||
|
||||
/**
|
||||
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
|
||||
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
|
||||
|
||||
@@ -8,9 +8,10 @@ import { AskThread } from './AskThread'
|
||||
import { BlockTools } from './BlockTools'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { KindIcon } from './kindIcons'
|
||||
import { PropertyGrid } from './PropertyGrid'
|
||||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||||
import { asksOf, historyOf } from './qa'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { translationTableId } from './viewState'
|
||||
|
||||
export interface BlockTreeState {
|
||||
version: number
|
||||
@@ -112,13 +113,21 @@ function BlockQa({ anchor, qa }: { anchor: string, qa: QaTree }): ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
/** 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文,带说明的译文下面接着说明。 */
|
||||
/**
|
||||
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
|
||||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。
|
||||
*/
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
return (
|
||||
<>
|
||||
<TextPreview text={block.translation} />
|
||||
{block.table && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyTable parentId={translationTableId(block.id)} columns={block.table.columns} items={block.table.items} />
|
||||
</div>
|
||||
)}
|
||||
{block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -96,9 +96,9 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
|
||||
|
||||
/**
|
||||
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它。
|
||||
*/
|
||||
function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
||||
export function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
||||
return (
|
||||
<div className="pl-[5px]">
|
||||
<div className="cr-table" role="table" style={{ gridTemplateColumns: tableTemplate(columns.length) }}>
|
||||
|
||||
@@ -95,6 +95,16 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||
})
|
||||
|
||||
it('名字在一行译文下面那张表里(跨几行写的调用的实参):表总是展开的,目标是那块,不用展开什么', () => {
|
||||
const call: BlockView = {
|
||||
...textBlock('0.0', 'return', 1, [word('返回'), word('新建'), name('UserDetailResponse', 10)]),
|
||||
table: { columns: ['参数', '值'], items: [{ cells: [[name('id', 40)], [name('user', 43), { text: '.', role: 'plain' }, name('id', 48)]] }] },
|
||||
}
|
||||
const blocks = [textBlock('0', 'if', 0, [word('如果'), name('user', 3)], [call])]
|
||||
expect(revealPlan(blocks, 9, 48)).toEqual({ id: '0.0', expand: ['0'] })
|
||||
expect(occurrencePlan(blocks, { start: 40, end: 42, kind: 'read' }, () => false)).toEqual({ id: '0.0', expand: ['0'] })
|
||||
})
|
||||
|
||||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||||
const blocks: BlockView[] = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockView, Occurrence, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import type { BlockView, Occurrence, PreviewTable, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import { blockPath } from './filter'
|
||||
import { showsOccurrence } from './occurrences'
|
||||
import { showsOffset } from './segments'
|
||||
@@ -38,19 +38,24 @@ function shows(text: PreviewText, offset: number): boolean {
|
||||
return !('markdown' in text) && showsOffset(text, offset)
|
||||
}
|
||||
|
||||
/** 一行译文下面那张表(总是展开的)里有格子满足 test。 */
|
||||
function tableShows(table: PreviewTable | undefined, test: (cell: PreviewText) => boolean): boolean {
|
||||
return table?.items.some(item => item.cells.some(test)) ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
|
||||
* 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* ③ 块里只是显示着这个名字(一行译文和它下面的表里的名字、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* 不在这块里是 undefined。
|
||||
*/
|
||||
function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
if (covers(block.defines, offset))
|
||||
return { expand: [] }
|
||||
if ('translation' in block)
|
||||
return shows(block.translation, offset) ? { expand: [] } : undefined
|
||||
return shows(block.translation, offset) || tableShows(block.table, cell => shows(cell, offset)) ? { expand: [] } : undefined
|
||||
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
|
||||
for (const { row, id } of rows) {
|
||||
if (covers(row.defines, offset))
|
||||
@@ -120,10 +125,10 @@ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, str
|
||||
return owner === undefined ? plan : { id: owner, expand: blockPath(owner).slice(0, -1) }
|
||||
}
|
||||
|
||||
/** 块里哪儿显示着满足 test 的文字(译文、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
/** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined {
|
||||
if ('translation' in block)
|
||||
return test(block.translation) ? [] : undefined
|
||||
return test(block.translation) || tableShows(block.table, test) ? [] : undefined
|
||||
for (const [index, row] of block.properties.entries()) {
|
||||
if (test(row.value))
|
||||
return []
|
||||
|
||||
@@ -71,6 +71,12 @@ export interface PropertyRow extends Defines {
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行 */
|
||||
export interface PreviewTable {
|
||||
columns: string[]
|
||||
items: PropertyItem[]
|
||||
}
|
||||
|
||||
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走 */
|
||||
export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
@@ -108,9 +114,9 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 函数、类、枚举、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文;
|
||||
* 一行译文的块带说明时 doc 是说明的 Markdown 原文,排在译文下面
|
||||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(table),块的说明(doc,Markdown 原文)
|
||||
*/
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
|
||||
@@ -90,3 +90,8 @@ export function propertyRowId(blockId: string, rowIndex: number): string {
|
||||
export function propertyItemId(parentId: string, itemIndex: number): string {
|
||||
return `${parentId}/${itemIndex}`
|
||||
}
|
||||
|
||||
/** 一行译文下面那张表(跨几行写的调用的实参)的 id:表里各行的 id 是 propertyItemId(它, 行号)。这张表总是展开的。 */
|
||||
export function translationTableId(blockId: string): string {
|
||||
return `${blockId}/t`
|
||||
}
|
||||
Reference in new issue
Block a user