From 15790ce7c88659a5033b90805497ab26484e6a96 Mon Sep 17 00:00:00 2001 From: cruldra Date: Sun, 27 Sep 2026 21:18:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E7=9A=84=E8=AF=8D=E5=8F=AF=E4=BB=A5=E8=AE=B2?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E9=87=8C=E5=88=AB=E5=A4=84=E7=9A=84=E5=90=8D?= =?UTF-8?q?=E5=AD=97=EF=BC=88Segment.refers=EF=BC=89=EF=BC=9AFastAPI=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=87=8C=E7=9A=84=20{user=5Fid}=20=E8=AE=B2?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=9A=84=E5=8F=82=E6=95=B0=EF=BC=8C=E5=8D=95?= =?UTF-8?q?=E5=87=BB=E5=8F=82=E6=95=B0=E6=97=B6=E8=B7=9F=E7=9D=80=E9=AB=98?= =?UTF-8?q?=E4=BA=AE=EF=BC=8C=E2=8C=98=20+=20=E7=82=B9=E5=87=BB=E8=B7=B3?= =?UTF-8?q?=E5=88=B0=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 13 +++++---- vscode/src/codeReader/contract.ts | 5 +++- vscode/src/codeReader/rules/fastapi.test.ts | 5 +++- vscode/src/codeReader/rules/fastapi.ts | 27 ++++++++++++++++--- .../src/codeReader/SegmentText.test.tsx | 11 ++++++++ .../webview-ui/src/codeReader/SegmentText.tsx | 8 +++--- .../src/codeReader/occurrences.test.ts | 12 ++++++++- .../webview-ui/src/codeReader/occurrences.ts | 18 ++++++++++--- vscode/webview-ui/src/codeReader/types.ts | 5 ++-- 9 files changed, 82 insertions(+), 22 deletions(-) diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 6fb7b64..7f11c8d 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -287,6 +287,7 @@ - 能点的名字和「跳到定义」一样:带源码下标的段(属性表、一行译文、字典 / 对象各层子行的键和值,点分模块名每一节各算各的)。「编辑」状态的块里的源码、Markdown 里的名字不算,既不能点也不被高亮。 - 单击名字:问扩展端(`vscode.executeDocumentHighlights`,文档 uri + 名字在源码里的位置),由语言扩展(Pylance、TypeScript 语言服务……)按语义答,同名的别的东西(例如同名的参数)不算。页面上源码范围和回答里任何一处重叠的段都高亮,一个名字被拆成几段(`luffy_agent.core.db`)时每段各按自己的范围认。 +- 讲别处名字的段(`Segment.refers`):写在字符串里、语言扩展不认得的名字,库规则标出它讲的是源码里哪个名字(FastAPI 路径 `"/{user_id}"` 里的 `user_id` 讲的是函数的参数 `user_id`)。页面把这段当那个名字:单击问那个名字的各处出现,⌘ / Ctrl + 点击跳到那个名字的定义(参数本身),那个名字的某处出现盖住 `refers` 时这段也高亮(单击参数 `user_id` 时 URL、路由器接口表里的 `{user_id}` 一起亮)。只有一处认这件事:webview 的 `occurrences.ts` 的 `sourceOf`(段讲的是源码里哪一段名字:有 `refers` 取它,否则是照抄的位置),`data-offset`、高亮、当前那一处都按它;扩展端不用管。 - 颜色和编辑器里一样:写入(定义、赋值,`DocumentHighlightKind.Write`)用 `--vscode-editor-wordHighlightStrongBackground`,读取和按文字认出的用 `--vscode-editor-wordHighlightBackground`;主题给了 `wordHighlightStrongBorder` / `wordHighlightBorder`(高对比主题)才描边,描边画在字框里面不挤动排版。同一段碰上两种按写入画。 - 很多主题没配单词高亮色(例如 Gerry Oceanic),默认的灰底很淡,所以再用主题的 `--vscode-focusBorder` 补一笔(换什么主题都跟着主题走):当前那一处(点中的、用键盘跳到的)在底色之外描一圈 1px 框;别的各处在底色下面画一条 1px 细线(内阴影,不占 `text-decoration`,⌘ 指着名字时的链接下划线照旧)。写入的那处照样是强高亮底色。框和线都画在字框里面,不挤动排版。 - 看不见的出现(在折叠着的块、属性行、子行里,或块被筛掉)点的时候不自动展开;筛选栏右边、块数前面写「名字 · 2/5」(当前那一处是照源码先后排的第几处 / 回答里共几处,看不见的也算),名字取点中的那一段的文字;点中的名字不在回答里(没有当前那一处)时写「名字 · N 处」。之后自己展开,里面的出现照样亮着。 @@ -569,8 +570,10 @@ export type TokenRole = * 第三方翻译插件可以不填,不填就不能跳。 * reference:只有库规则给的段有,这段讲的东西由规则自己查定义(LibraryRule.resolve),不问语言扩展;这样的段必须带 offset。 * color:这段自己的颜色(暗色、亮色主题各一个),盖过词性的颜色:库规则按库自己的配色上色(FastAPI 的 HTTP 方法用 Swagger UI 的颜色)。 + * refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字(FastAPI 路径里的参数名)。 + * 预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,单击高亮它的各处出现,它的某处出现盖住 refers 时这段也高亮;offset 照旧是照抄的位置。 */ -export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor } +export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange } /** 框的样式,决定框头图标和框的颜色:'iteration' 是写在表达式里的遍历,图标、颜色同「循环」块。 */ export type GroupStyle = 'iteration' @@ -1038,7 +1041,7 @@ export interface CodeReaderApi { 请求表: -- URL:路由对象的 `prefix` 加装饰器的路径;`{参数}` 按 variable 色,其余按 string 色,都带源码下标;写的不是字符串(`prefix=settings.X`)交给翻译插件翻。 +- URL:路由对象的 `prefix` 加装饰器的路径;`{参数}` 按 variable 色(拆成 `{`、名字、`}` 三段),其余按 string 色,都带源码下标;花括号里的名字是函数的同名参数时标 `refers`(参数名的范围):单击参数 `user_id` 时 URL 里的 `user_id` 跟着亮,⌘ + 点击 URL 里的 `user_id` 跳到参数;写的不是字符串(`prefix=settings.X`)交给翻译插件翻。 - 方法:大写,颜色用 Swagger UI 的配色(段的 `color`):GET #61affe、POST #49cc90、PUT #fca130、PATCH #50e3c2、DELETE #f93e3e(暗色);白底上照原色看不清,亮色主题取同色相深一些的 #1b6ac9、#18834f、#b35f00、#0f8a70、#cf2020;HEAD #b36bff / #9012fe、OPTIONS #4b92e0 / #0d5aa7(Swagger UI 的 #9012fe、#0d5aa7 在暗底上看不清,暗色调亮了);TRACE 没有配色,按 keyword 色。 - 路径参数、查询参数、请求头、Cookie:`名称: 类型`,逗号隔开,名称 variable 色带下标,类型交给翻译插件翻(`Annotated[T, …]` 写 T);没标注的只写名称。 - 请求体:只有一个时写它的类型(模型名),几个时(表单的几个字段)写 `名称: 类型` 一串。 @@ -1379,7 +1382,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。 - 行替换:多行块替换后缩进正确。 - 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。 -- 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(`occurrences.test.ts`、`reveal.test.ts`、`editor/occurrenceNavigation.test.ts`):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;`package.json` 的两条命令、默认键、`when`、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了 `KEY_SCROLL_MS` 或又按键、点鼠标不守。 +- 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(`refers`)按那个名字算、自己照抄的位置不算(`occurrences.test.ts` 的 `sourceOf`),点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(`occurrences.test.ts`、`reveal.test.ts`、`editor/occurrenceNavigation.test.ts`):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;`package.json` 的两条命令、默认键、`when`、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了 `KEY_SCROLL_MS` 或又按键、点鼠标不守。 - 中文名: - 语言插件认写法(`languages/pythonNames.test.ts`、`languages/typescriptNames.test.ts`,真实解析):Python 注解 `Annotated`(`typing.Annotated`、`t.Comment`,f-string、别的调用不算)、装饰器(函数、类)、行尾记号(多行参数表、`*args`、`self.x`、变量、字段、枚举成员,记号前面有别的字,记号的范围)、单独成行 / 那一行没有定义的不算、函数那一行给参数、docstring 的谷歌写法和 Sphinx 写法(带类型、星号、续行、别的段落不算)、第一小句和 12 个字的限制、明写的记号盖过说明文字、没取的记号照样去掉;TypeScript 装饰器(类、`@d export class`、字段、构造函数和方法的参数、方法隔着别的装饰器、名字最后一节)、行尾 `//` 记号(参数、变量、枚举成员、类字段、接口属性,块注释不算)、JSDoc `@param`(类型、短横、可选、属性不算、export 函数、方法、箭头函数变量、类字段)、JavaScript 默认值参数;预筛和各方言的文件。 - 记号不是说明(`nameResolution.test.ts`,走 `readBlockViews`):Python、TypeScript 枚举成员的行尾记号去掉、前面的字照留、只有记号的没有说明列。 @@ -1396,9 +1399,9 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - Celery 规则:`fixtures/celeryApp.py` 的应用卡片和定时任务卡片逐字同上面的样例(列名、「12 条,另有展开 1 处」、行尾注释、条目里单独成行的注释、跨行的 `10⏎ * 60`、`每 get_settings().… 秒`、展开一行);任务名是引用、下标指回源码原文;应用卡片的位置参数按参数顺序认、别的参数各一行、展开、属性 docstring、别名;没导入 celery、不是 `Celery(...)` 的不认;频率换算(秒数算式、`timedelta`、`crontab` 各种写法、`schedule`、换算不了的「每 X 秒」和照写法);说明取哪些注释;条目名和任务名不同、没写任务名、任务名不是字符串、值不是字典、选项一列有才有;`conf.update(beat_schedule=…)`、不带 `beat_schedule` 的 `conf.update` 和 `autodiscover_tasks` 照常。任务名索引:显式 `name=`、默认「模块路径.函数名」(包、`src` 下的命名空间包、没有包)、`@shared_task` / `@shared_task()` / `@app.task` 不带括号 / 老写法 `@task`、`async def`、函数里的只认显式名字、`name=` 不是字符串的不认、同名几处都给、文件改了和新建 `__init__.py` 后跟着变、没有任务装饰器的文件不解析。 - 枚举(`rules/enum.test.ts`、`languages/typescript.test.ts`):`fixtures/enumModels.py` 的 `CommissionStatus` 逐字同上面的样例,`SecretScope` 的 docstring 当说明、没有说明列;种类 enum、叫「枚举」、访问级别同原来的类、没有标签(badge),编辑范围是整个类;Python 成员规则(dunder、sunder、私有名、`_ignore_`、lambda、`property`、`nonmember`、只写注解的不是成员,`auto()` 写「自动」,`member(x)` 取 x,说明取上面的注释块、行尾注释、属性 docstring,装饰器);方法、嵌套类、不是成员的属性照源码顺序是子块,成员前面的注释并进来、最后面的留下,「编辑」写回整个类;类名、成员名的定义范围和带下标的词指回源码原文;没导入 enum、SQLAlchemy 的 `Enum`、不是枚举类型的父类不认,`import enum as e` 的 `e.IntEnum` 认。TypeScript:`enums.ts` 里 export / const / declare / 函数里的 enum 都是枚举块、JSDoc 进头部,写的值、自增算出的值(接不上数字就没有)、说明(上面的注释和 JSDoc、行尾注释,同一行的 JSDoc 讲下一个),访问级别;自增的值和「自动」按 hint。TypeScript 和 Python 同样的成员得到一样的种类、属性表行、列名和格子着色(除了 Python 多的「继承」)。宿主:规则认成现成属性表块时照语言插件报的这种块显示、并进来的不是子块时照常显示;webview 枚举块默认折叠。 - 数据类(`rules/dataclass.test.ts`):`fixtures/dataclassModels.py` 的 `SandboxAccess` 逐字同上面的样例,`AdminUserRow`、`RedirectPayResult`(`field(init=False, default=…)`)、`SmsSent`(`default_factory=dict`)的行;种类 `dataclasses.dataclass`、叫「数据类」、颜色、访问级别同原来的类;编辑范围是整个类、写回原样,`__post_init__` 是子块;类名、字段名的定义范围和带下标的词指回源码原文;装饰器的写法(别名、`dataclasses.dataclass`、`dc.dataclass`)认,pydantic 的、`other.dataclass` 不认,没导入 dataclasses 不认;每个选项的译法、默认值不讲、别的照写、`**x`、别的装饰器一行;字段规则(`ClassVar`、`typing.ClassVar` 留作子块,`InitVar` 的类型和「仅初始化时使用」,`KW_ONLY` 记号并进来、后面的字段「只能按名字传参」、单独写 `kw_only=False` 的「可按位置传参」),`field(...)` 的默认值、`default_factory`(名字、lambda)、各参数、`metadata` 照写、`doc=`,说明取上面的注释块、行尾注释、属性 docstring;没注解的类属性、字段中间的方法照源码顺序是子块,「编辑」写回整个类。 -- FastAPI 接口(`rules/fastapi.test.ts`):`fixtures/routes.py` 的 `update_user` 逐字同上面的样例(查到 `Forbidden` 是 403 时四条消息写在一行,没查到时不列),名字、颜色、图标、列名、筛选属性、定义范围、编辑范围是整个函数、函数体是子块;着色(URL 的 `{参数}` / 路径、方法的 Swagger 颜色、成功的状态码绿色、自动带的 422 淡色,带下标的词指回源码原文);`fixtures/skillHubRoutes.py` 的 `list_skills`(Annotated 里的 Query、`_` 的依赖)、`get_skill`(HTTPException 的 status_code / detail)、`install`(Request 不算、`-> None` 没有内容);`fixtures/consoleRoutes.py` 的 `create_user`(`status_code=201`、Pydantic 模型、调别的函数间接抛的不算)、`list_users`(查到的类型别名 `FilterQuery` 是依赖、Literal 是简单类型);参数放在哪儿的各条(路径里的名字含 `{x:path}`、各种标记两种写法、上传文件、枚举和类型别名查到的、Pydantic 模型、`Security`、`Depends()`);标签、依赖、`responses=` 叠上路由对象的,`status.HTTP_…` 常量、`response_model=None`、`response_description`、`raise … from`、只写 `raise`、嵌套函数里的;`FastAPI()` 上的接口、别的装饰器一行;不认的(没导入、路由对象在别的文件、不是路由对象、`websocket`、不带括号)。跨文件(工作区索引 + 假的语言扩展):`update_user` 的 `Forbidden` 按定义处查到 `core/exceptions/schemas.py` 的 403,同名的别处的类不算;索引认的类上的 `status_code`(带注解、常量)、枚举类、`Annotated` 类型别名,普通类不认。 +- FastAPI 接口(`rules/fastapi.test.ts`):`fixtures/routes.py` 的 `update_user` 逐字同上面的样例(查到 `Forbidden` 是 403 时四条消息写在一行,没查到时不列),名字、颜色、图标、列名、筛选属性、定义范围、编辑范围是整个函数、函数体是子块;着色(URL 的 `{参数}` / 路径,花括号里的参数名 `refers` 到函数的参数,方法的 Swagger 颜色、成功的状态码绿色、自动带的 422 淡色,带下标的词指回源码原文);`fixtures/skillHubRoutes.py` 的 `list_skills`(Annotated 里的 Query、`_` 的依赖)、`get_skill`(HTTPException 的 status_code / detail)、`install`(Request 不算、`-> None` 没有内容);`fixtures/consoleRoutes.py` 的 `create_user`(`status_code=201`、Pydantic 模型、调别的函数间接抛的不算)、`list_users`(查到的类型别名 `FilterQuery` 是依赖、Literal 是简单类型);参数放在哪儿的各条(路径里的名字含 `{x:path}`、各种标记两种写法、上传文件、枚举和类型别名查到的、Pydantic 模型、`Security`、`Depends()`);标签、依赖、`responses=` 叠上路由对象的,`status.HTTP_…` 常量、`response_model=None`、`response_description`、`raise … from`、只写 `raise`、嵌套函数里的;`FastAPI()` 上的接口、别的装饰器一行;不认的(没导入、路由对象在别的文件、不是路由对象、`websocket`、不带括号)。跨文件(工作区索引 + 假的语言扩展):`update_user` 的 `Forbidden` 按定义处查到 `core/exceptions/schemas.py` 的 403,同名的别处的类不算;索引认的类上的 `status_code`(带注解、常量)、枚举类、`Annotated` 类型别名,普通类不认。 - 规则查别的文件里的定义(`ruleLookups.test.ts`):第一遍当没查到、记下问了什么(同一索引同名的只记一处);查到后叫重排,交出定义处正是的那一项;同一版本不再查;索引里没有同名的不问语言扩展,定义处不是索引里的项算没有,没变不叫重排;新版本先用上一版的、再查、变了才叫重排;语言扩展没答时已知的不动、隔一会儿再查。 -- 带自己颜色的词(webview `SegmentText.test.tsx`):`cr-colored`,暗色、亮色写在 style 上。 +- 带自己颜色的词(webview `SegmentText.test.tsx`):`cr-colored`,暗色、亮色写在 style 上;讲别处名字的段 `data-offset` 记那个名字的下标。 - 异常(`languages/exceptions.test.ts`,真实解析 + 中文直译 + 视图数据):`fixtures/appExceptions.py` 的 `AppException`、`NotFound` 逐字同「异常」的样例(列名随有没有类型、说明变),`GatewayTimeout` 的多段说明,方法是子块;`fixtures/commerceExceptions.py` 16 个类都是异常、只有说明的四行、写了 `__init__` 的构造参数;认哪些类(自带的异常类、`Warning`、不合后缀的自带类 `StopIteration` / `KeyboardInterrupt`、`pkg.errors.AppException`、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、`Mapping[...]`、`*bases` 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、`*args` 开头的不去;`@dataclass` 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript `fixtures/exceptions.ts.txt`(`extends Error`、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、`ns.X`,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(`viewState.test.ts`),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(`reveal.test.ts`)。 - 筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;全选 / 取消全选只动这份文件里的值、别的文件的去掉状态和别的属性不动、取消全选藏掉有这个属性的块(整棵子树)、再全选全回来;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在 `workspaceState`、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。 - 文件注释:键相对工作区文件夹、`/` 分隔,根和外面的路径没有键,手写的 `./`、结尾 `/` 规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建 `.spx`、键排序、删光留 `{}`;有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和 `package.json` 里菜单的 when 子句对得上。 diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index d3263f2..6211e46 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -275,8 +275,11 @@ export type TokenRole * 它在源码里的起始下标(同 SourceSpan 的下标),预览拿它向 VS Code 查定义、跳过去;译词、提示词没有。 * reference:只有库规则给的段有,这段讲的东西由规则自己查定义(LibraryRule.resolve),不问语言扩展;这样的段必须带 offset。 * color:这段自己的颜色(暗色、亮色主题各一个),盖过词性的颜色:库规则按库自己的配色上色(FastAPI 的 HTTP 方法用 Swagger UI 的颜色)。 + * refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字 + * (FastAPI 路径 `"/{user_id}"` 里的 user_id 讲的是函数的参数 user_id)。预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义, + * 单击高亮它的各处出现,那个名字的某处出现盖住 refers 时这段也跟着高亮。offset 照旧是这段照抄的位置。 */ -export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor } +export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange } /** 框的样式,决定框头的图标和框的颜色:'iteration' 是写在表达式里的遍历,图标、颜色同「循环」块。 */ export type GroupStyle = 'iteration' diff --git a/vscode/src/codeReader/rules/fastapi.test.ts b/vscode/src/codeReader/rules/fastapi.test.ts index f8446a5..856a64f 100644 --- a/vscode/src/codeReader/rules/fastapi.test.ts +++ b/vscode/src/codeReader/rules/fastapi.test.ts @@ -95,9 +95,12 @@ describe('fastapi 库规则:接口卡片', () => { const rows = properties(await route(ROUTES, 'update_user', known({ Forbidden: '403' }))) const request = rows.find(row => row.label === '请求') const [url, method] = request?.items ?? [] - expect(flatSegments(url.cells[1]).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{user_id}', 'variable']]) + expect(flatSegments(url.cells[1]).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{', 'variable'], ['user_id', 'variable'], ['}', 'variable']]) for (const piece of flatSegments(url.cells[1])) expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text) + // 路径里的 user_id 讲的是 update_user 的参数 user_id:高亮参数的各处出现时跟着亮,⌘ + 点击跳到参数 + const param = ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf('def update_user')) + expect(flatSegments(url.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: param, endIndex: param + 'user_id'.length }) expect(method.cells[1]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }]) const [ok, forbidden, validation] = rows.find(row => row.label === '响应')?.items ?? [] expect(ok.cells[0]).toEqual([{ text: '200', role: 'number', color: { dark: '#2ea043', light: '#1a7f37' } }]) diff --git a/vscode/src/codeReader/rules/fastapi.ts b/vscode/src/codeReader/rules/fastapi.ts index a0ccc21..b1b9b1b 100644 --- a/vscode/src/codeReader/rules/fastapi.ts +++ b/vscode/src/codeReader/rules/fastapi.ts @@ -266,8 +266,11 @@ function routeOf(decorator: Node, routers: ReadonlyMap): Route | return { decorator, router, method: method.toUpperCase(), call, path: call.positional[0] ?? call.keywords.get('path') } } -/** 路径里的一段:`{参数}` 按变量着色,其余按字符串,带源码下标;写的不是字符串时交给翻译插件翻。 */ -function pathSegments(node: Node | undefined, env: Env): Inline[] { +/** + * 路径里的一段:`{参数}` 按变量着色,其余按字符串,都带源码下标;花括号里的名字讲的是函数的同名参数(params), + * 标上 refers,高亮参数的各处出现时它也跟着亮、⌘ + 点击跳到参数。写的不是字符串时交给翻译插件翻。 + */ +function pathSegments(node: Node | undefined, params: ReadonlyMap, env: Env): Inline[] { if (!node) return [] const content = stringContent(node) @@ -276,7 +279,22 @@ function pathSegments(node: Node | undefined, env: Env): Inline[] { const segments: Segment[] = [] let at = content.node?.startIndex ?? node.startIndex for (const part of content.text.split(/(\{[^}]*\})/).filter(Boolean)) { - segments.push({ text: part, role: part.startsWith('{') ? 'variable' : 'string', offset: at }) + if (!part.startsWith('{')) { + segments.push({ text: part, role: 'string', offset: at }) + } + else { + // `{file_path:path}`:冒号前面是参数名,后面是转换器 + const colon = part.indexOf(':') + const key = part.slice(1, colon < 0 ? -1 : colon) + const rest = part.slice(1 + key.length, -1) + const param = params.get(key.trim()) + const pieces: Segment[] = [ + { text: '{', role: 'variable', offset: at }, + { text: key, role: 'variable', offset: at + 1, ...(param && { refers: range(param) }) }, + { text: `${rest}}`, role: 'variable', offset: at + 1 + key.length }, + ] + segments.push(...pieces.filter(piece => piece.text !== '')) + } at += part.length } return segments @@ -537,7 +555,8 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard { const at = (place: Place): Placed[] => placed.filter(item => item.place === place) const body = at('body') - const url = [...pathSegments(router.prefix, env), ...pathSegments(route.path, env)] + const params = new Map(facts.parameters.flatMap(({ name: param }) => param.type === 'identifier' ? [[param.text, param] as const] : [])) + const url = [...pathSegments(router.prefix, params, env), ...pathSegments(route.path, params, env)] const request: PropertyItem[] = [ requestItem(words.items.url, url), requestItem(words.items.method, [{ text: route.method, role: 'keyword', ...(METHOD_COLORS[route.method] && { color: METHOD_COLORS[route.method] }) }]), diff --git a/vscode/webview-ui/src/codeReader/SegmentText.test.tsx b/vscode/webview-ui/src/codeReader/SegmentText.test.tsx index f3efbdd..b84b172 100644 --- a/vscode/webview-ui/src/codeReader/SegmentText.test.tsx +++ b/vscode/webview-ui/src/codeReader/SegmentText.test.tsx @@ -19,4 +19,15 @@ describe('着色的词', () => { ) expect(html).toBe('PUT /users') }) + + it('讲别处名字的段(refers):data-offset 记那个名字的下标,⌘ + 点击、单击高亮都按它', () => { + const html = renderToStaticMarkup( + , + ) + expect(html).toBe('/users/user_id') + }) }) diff --git a/vscode/webview-ui/src/codeReader/SegmentText.tsx b/vscode/webview-ui/src/codeReader/SegmentText.tsx index b1234da..b997884 100644 --- a/vscode/webview-ui/src/codeReader/SegmentText.tsx +++ b/vscode/webview-ui/src/codeReader/SegmentText.tsx @@ -4,12 +4,12 @@ import type { GroupStyle, Inline, SegmentGroup } from './types' import { useContext } from 'react' import { DisplayNameContext, shownName } from './displayNames' import { KIND_ICON_COMPONENTS, KIND_ICONS } from './kindIcons' -import { isCurrent, segmentOccurrence } from './occurrences' +import { isCurrent, segmentOccurrence, sourceOf } from './occurrences' import { HighlightContext } from './useOccurrences' /** - * 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上, - * 跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时 + * 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上 + * (讲别处名字的段记那个名字的下标,见 sourceOf),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时 * 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。 * 框起来的一组词画成小框,框里的词同样处理。 */ @@ -27,7 +27,7 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement key={index} className={piece.color ? `cr-role-${piece.role} cr-colored` : `cr-role-${piece.role}`} style={piece.color && { '--color-dark': piece.color.dark, '--color-light': piece.color.light } as CSSProperties} - data-offset={piece.offset} + data-offset={sourceOf(piece)?.start} data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)} data-current={highlight && isCurrent(piece, highlight) ? '' : undefined} data-name={shown?.name} diff --git a/vscode/webview-ui/src/codeReader/occurrences.test.ts b/vscode/webview-ui/src/codeReader/occurrences.test.ts index c094b12..aefceb6 100644 --- a/vscode/webview-ui/src/codeReader/occurrences.test.ts +++ b/vscode/webview-ui/src/codeReader/occurrences.test.ts @@ -6,7 +6,7 @@ import { createPythonAdapter } from '../../../src/codeReader/languages/python' import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport' import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh' import { DefinitionLinks } from './definitionLinks' -import { clickAction, isCurrent, KEY_SCROLL_MS, KEY_TO_COMMAND_MS, KeyScroll, occurrenceCount, OccurrenceHighlight, OccurrenceNavigation, occurrenceStyle, segmentOccurrence, stepIndex } from './occurrences' +import { clickAction, isCurrent, KEY_SCROLL_MS, KEY_TO_COMMAND_MS, KeyScroll, occurrenceCount, OccurrenceHighlight, OccurrenceNavigation, occurrenceStyle, segmentOccurrence, sourceOf, stepIndex } from './occurrences' function name(text: string, offset: number): Segment { return { text, role: 'variable', offset } @@ -54,6 +54,16 @@ describe('occurrences:哪些段画高亮', () => { expect(segmentOccurrence(name('a', 0), [at(0, 1, 'read'), at(0, 1, 'write')])).toBe('strong') }) + it('讲别处名字的段(refers,写在字符串里的参数名)按那个名字算:那个名字的出现盖住它就画、能是当前那一处,自己照抄的位置不算', () => { + const param: Segment = { text: 'user_id', role: 'variable', offset: 40, refers: { startIndex: 100, endIndex: 107 } } + expect(sourceOf(param)).toEqual({ start: 100, end: 107 }) + expect(segmentOccurrence(param, [at(100, 107, 'write')])).toBe('strong') + expect(segmentOccurrence(param, [at(40, 47)])).toBeUndefined() + expect(isCurrent(param, { name: 'user_id', occurrences: [at(100, 107)], current: 0 })).toBe(true) + expect(sourceOf(name('x', 3))).toEqual({ start: 3, end: 4 }) + expect(sourceOf({ text: '常量', role: 'storage' })).toBeUndefined() + }) + it('真实的 Python 译文和属性表:定义处按写入、用到的地方按读取画;点分模块名每一节各按自己的范围认', async () => { const source = [ 'import luffy_agent.core.db', diff --git a/vscode/webview-ui/src/codeReader/occurrences.ts b/vscode/webview-ui/src/codeReader/occurrences.ts index 2f14a83..781bea7 100644 --- a/vscode/webview-ui/src/codeReader/occurrences.ts +++ b/vscode/webview-ui/src/codeReader/occurrences.ts @@ -8,7 +8,7 @@ export function occurrenceStyle(kind: Occurrence['kind']): OccurrenceStyle { } /** - * 一段文字画不画高亮、画哪种:它照抄自源码,在源码里占 [offset, offset + 字数),和哪处出现重叠就画, + * 一段文字画不画高亮、画哪种:它讲的名字(sourceOf)和哪处出现重叠就画, * 同时碰上写入和读取的按写入画。没有下标的(译词、提示词、编辑状态的源码)不画。 */ export function segmentOccurrence(segment: Segment, occurrences: readonly Occurrence[]): OccurrenceStyle | undefined { @@ -23,10 +23,20 @@ export function segmentOccurrence(segment: Segment, occurrences: readonly Occurr return style } -/** 段照抄自源码、在源码里占的 [offset, offset + 字数) 和这处出现重叠;没有下标的段不算。 */ +/** + * 段讲的是源码里哪个名字,[start, end):讲别处名字的段(refers,写在字符串里的参数名)是那个名字,照抄自源码的段是它自己占的 + * [offset, offset + 字数);没有下标的段(译词、提示词)没有。跳到定义、单击高亮、各处出现画在哪都按它。 + */ +export function sourceOf(segment: Segment): { start: number, end: number } | undefined { + if (segment.refers) + return { start: segment.refers.startIndex, end: segment.refers.endIndex } + return segment.offset === undefined ? undefined : { start: segment.offset, end: segment.offset + segment.text.length } +} + +/** 段讲的名字(sourceOf)和这处出现重叠;没有下标的段不算。 */ export function overlaps(segment: Segment, occurrence: Occurrence): boolean { - const { offset } = segment - return offset !== undefined && occurrence.start < offset + segment.text.length && offset < occurrence.end + const source = sourceOf(segment) + return source !== undefined && occurrence.start < source.end && source.start < occurrence.end } /** 文字里(连框里)有段和这处出现重叠。 */ diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index d4d6937..6f28096 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -17,9 +17,10 @@ export type TokenRole /** * offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有。 * reference:库规则自己查定义的名字(Celery 的任务名)才有;webview 不看它,照样只报 offset,扩展端按 offset 认出来交给规则查。 - * color:这段自己的颜色(库规则按库自己的配色给的,例如 HTTP 方法),盖过词性的颜色,按主题挑暗色、亮色 + * color:这段自己的颜色(库规则按库自己的配色给的,例如 HTTP 方法),盖过词性的颜色,按主题挑暗色、亮色。 + * refers:这段讲的是源码里别处的一个名字(写在字符串里的名字讲的参数):跳到定义、高亮各处出现都当它是那个名字,见 sourceOf */ -export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor } +export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange } /** 规则自己查得到定义的一个名字:哪个规则、规则自己认的键 */ export interface RuleReference { rule: string, key: string }