From f6bc217e915bfb9677d6be4b2f5ae71adc3c1f10 Mon Sep 17 00:00:00 2001 From: cruldra Date: Mon, 5 Oct 2026 05:40:09 +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=E5=85=A5=E5=8F=A3=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=92=8C=E5=85=A5=E5=8F=A3=E5=9D=97=E6=8D=A2=E6=88=90=E7=BB=BF?= =?UTF-8?q?=E8=89=B2=E6=92=AD=E6=94=BE=E5=9B=BE=E6=A0=87=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=86=99=E3=80=8C=E5=85=A5=E5=8F=A3=E3=80=8D=E4=BA=8C?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 12 ++--- vscode/src/codeReader/blockView.ts | 2 +- vscode/src/codeReader/contract.ts | 3 +- .../codeReader/editor/documentMessage.test.ts | 53 ++++++++++++++++--- .../src/codeReader/editor/documentMessage.ts | 8 +-- vscode/src/codeReader/editor/previewEditor.ts | 6 +-- vscode/src/codeReader/entries/entryMarks.ts | 31 +++++------ .../codeReader/explorer/rowDecoration.test.ts | 16 ++++-- .../src/codeReader/explorer/rowDecoration.ts | 20 +++++-- .../src/codeReader/explorer/rowDecorator.ts | 12 +++-- 10 files changed, 110 insertions(+), 53 deletions(-) diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 91c38e6..cc9a819 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -31,7 +31,7 @@ | 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠,外加给目录 / 文件记注释、按审查工单只看 PR 改过的文件;不做新建 / 重命名 / 删除 | | 审查工单 | 标题栏第一个按钮「选工单」列出看板「审查」列的工单和「进行中」列里已经有 PR 的工单(按列分组),选中后树里只剩这张工单的 PR 改过的文件(目录层级照留、全部展开,按 M / A / D 着色,删掉的列出来但打不开);点开的是本机这张工单的工作副本里 PR 最新的代码,不标差异、不开 diff;本机没有工作副本就拉 PR 分支建一个。弹框列的是看板上现在摆着的工单(不再加载一遍),选中后重读那张工单和它的 PR。见「审查工单」 | | 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 `.spx/code-notes.json`,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 | -| 入口 | 只认项目自己写明的启动项:pyproject.toml 的 `[project.scripts]` / `[project.gui-scripts]`、Cargo.toml 的 `src/main.rs` 和 `[[bin]]` 的 `path`、go.mod 模块里 `package main` 加 `func main` 的文件、package.json 的 `bin`;不按文件名、函数名猜。侧栏灰字「入口」(有注释时「注释 · 入口」),预览里名字对上的块名称后面加浅色「入口」。见「入口」 | +| 入口 | 只认项目自己写明的启动项:pyproject.toml 的 `[project.scripts]` / `[project.gui-scripts]`、Cargo.toml 的 `src/main.rs` 和 `[[bin]]` 的 `path`、go.mod 模块里 `package main` 加 `func main` 的文件、package.json 的 `bin`;不按文件名、函数名猜。侧栏里入口文件的图标换成绿色播放图标,预览里名字对上的块左边的种类图标换成同样的绿色播放图标。见「入口」 | | 问 AI | 块右上角「问 AI」,在块下面问 Claude Code(`claude -p`,只读,用输入框旁边选的 profile,记住上次选的,没选过用插件默认的)这块代码,回答逐字出来按 Markdown 显示、能追问(接着这块最近一次的会话,收起再展开、重启 VS Code 也接得上)、能停;输入框里能粘贴 / 拖进图片随问题发(存在 `.spx/code-qa/`,跟着问答删);问答存进各工作区文件夹的 `.spx/code-qa.json`,灰字挂在块下面免得重复问,理解了就删。问答按块的位置记号挂,记号只由和语言无关的块数据(种类、facts 里的名字、源码哈希)算出,文件别处改了照样认得;认不回来的不显示,留在文件里,代码改回来又挂回去。见「问 AI」 | | 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) | | 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 | @@ -85,7 +85,7 @@ - 在 VS Code 里改名、挪位置(资源管理器拖动、`WorkspaceEdit` 等,走 `workspace.onDidRenameFiles`):注释跟着走,目录连带里面各层的注释一起走;挪到另一个工作区文件夹就搬到那边的文件里,挪出工作区就不要了。删除(`onDidDeleteFiles`):连带目录里各层的注释一起删。在 VS Code 外面(终端、git)改名删除不跟。 - 外部改了 `code-notes.json`(git pull、手改):文件监听重读,树上跟着变。 - 刷新:只重画注释变了的那几行(文件树按 Uri 记住交给 VS Code 的节点对象,VS Code 按对象认行);还没展开过的行不用管。 -- 注释和代码阅读的解析、翻译无关,单独放在 `notes/`;文件树只留一个「给行添信息」的接口(`FileTreeDecorator`),由 `explorer/rowDecorator.ts` 的 `RowDecorator` 把注释和项目入口(见下节)合成一行的灰字和提示(纯函数 `explorer/rowDecoration.ts`),`index.ts` 接上。 +- 注释和代码阅读的解析、翻译无关,单独放在 `notes/`;文件树只留一个「给行添信息」的接口(`FileTreeDecorator`),由 `explorer/rowDecorator.ts` 的 `RowDecorator` 把注释和项目入口(见下节)合成一行的灰字、提示和图标(纯函数 `explorer/rowDecoration.ts`),`index.ts` 接上。 ### 入口 @@ -101,8 +101,8 @@ - 不认的:`[tool.poetry.scripts]` 等别的表、`[[bin]]` 没写 `path` 的、`src/bin/*.rs` 自动发现、`src/lib.rs`、package.json 的 `main` / `exports` / `scripts`。 - 一个工作区文件夹里可以有很多个项目(子目录各有自己的清单),各算各的;同一个文件被几条指到,名字合在一起。不进 `node_modules`、`.venv`、`vendor`、`dist`、`__pycache__`(还有 `.git`)。 - 什么时候算(`entries/workspaceEntries.ts` 的 `WorkspaceEntries`):打开时每个工作区文件夹扫一遍(`findFiles` 找清单和 `.go`,排除上面那几个目录);清单、`.go` 改了,或文件增删了(入口文件出现、消失),过 500ms 重扫那个工作区文件夹;入口变了的文件报给侧栏重画那几行、开着的预览重新发文档。 -- 侧栏:入口文件那一行名字后面的灰字,没注释时是「入口」,有注释时是「注释 · 入口」,不盖掉注释;悬停提示在注释下面再写一行「入口,从 `app` 开始读」(没有名字的写「入口」)。contextValue 不变,菜单照旧只按有没有注释换。目录、工作区外面的行(审查的工作副本)不标。 -- 预览:入口文件里,文件最外层、名字对上的那一块(函数、常量、变量、库规则的卡片,谁对上就是谁;Python 的 `app = typer.Typer()` 是变量块),属性表里写着这个名字的那一行、名字后面加一个浅色的「入口」——就是属性表里的说明词(`hint`),不另做标记。宿主在翻译好的结果上加(`entries/entryMarks.ts` 的 `markEntryNames`,`documentMessage` 里调),各语言的翻译插件不用管:块的名字按定义处(`defines`)在源码里的那一段认,属性表里按词的源码下标找到写名字的那个词;对不上就什么都不加。标差异时旧版本按同样的入口标,「入口」不会被当成改动。 +- 侧栏:入口文件那一行的文件图标换成绿色播放图标(VS Code 自带图标字体的 `play`,颜色是主题色 `testing.iconPassed`,即测试通过的那种绿;`explorer/rowDecoration.ts` 的 `ENTRY_ICON`)。只换代码阅读侧栏这棵树的行,资源管理器、标签页不变;git 改动的颜色和 M / A 字母照旧。名字后面的灰字只是注释,没注释时没有灰字,不写「入口」;悬停提示在注释下面再写一行「入口,从 `app` 开始读」(没有名字的写「入口」)。contextValue 不变,菜单照旧只按有没有注释换。目录、工作区外面的行(审查的工作副本)不标。 +- 预览:入口文件里,文件最外层、名字对上的那一块(函数、常量、变量、库规则的卡片,谁对上就是谁;Python 的 `app = typer.Typer()` 是变量块),卡片左边的种类图标换成绿色播放图标,名称后面不加字。走概念图标那条路:入口是一个概念(id `entry`、名字「入口」、图标 `play`、颜色 `var(--vscode-testing-iconPassed)`,和侧栏同一个主题色;`entries/entryMarks.ts` 的 `ENTRY_CONCEPT`),换掉概念列表的第一个(种类),后面别的概念照旧(库规则的卡片画几个图标的,入口替掉第一个,规则的种类和另认的照旧);种类、筛选、卡片边框色都不变。webview 照常按概念列表画,不特判入口。宿主在翻译好的结果上换(`entries/entryMarks.ts` 的 `markEntryBlocks`,`documentMessage` 里调),各语言的翻译插件不用管:块的名字按定义处(`defines`)在源码里的那一段认;对不上就什么都不改。标差异时旧版本按同样的入口标,两边的入口块一样,不会被当成改动。 ### 审查工单 @@ -2062,10 +2062,10 @@ vscode/src/codeReader/ notes/store.ts .spx/code-notes.json 叫什么、每条怎么认 notes/fileNotes.ts FileNotes:注释的 FolderStore、一行的注释 notes/commands.ts 「添加注释 / 编辑注释」输入框 - explorer/rowDecoration.ts 一行名字后面的灰字、悬停提示、contextValue:注释,后面接入口(纯函数) + explorer/rowDecoration.ts 一行名字后面的灰字(注释)、悬停提示、contextValue,入口文件的绿色播放图标(纯函数) explorer/rowDecorator.ts RowDecorator:把注释和入口交给 rowDecoration 画行(FileTreeDecorator) entries/entryPoints.ts 项目写明的入口:四种清单 / Go 源码 → 入口文件和名字(纯函数,读文件经 FolderReader) - entries/entryMarks.ts 预览里名字对上的块,名称后面加「入口」(纯函数) + entries/entryMarks.ts 预览里名字对上的块,概念列表第一个换成入口(绿色播放图标)(纯函数) entries/workspaceEntries.ts WorkspaceEntries:各工作区文件夹扫一遍、文件变了重扫、报出入口变了的文件 qa/anchor.ts 块的位置记号(BlockAnchors)(不碰 VS Code) qa/qaMap.ts .spx/code-qa.json 的形状、每条怎么认、记一次 / 删一次、再问时接着哪个会话(纯函数) diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index e700a5c..32a9391 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -33,7 +33,7 @@ interface BlockViewBase { attributes: BlockAttributes /** * 这块认出来的概念,从左到右:语言的种类(再加语言插件另认的),库规则的卡片再加规则的种类和规则另认的。 - * 同一个 id 只留第一个。webview 按这个画图标,不自己用种类查。 + * 同一个 id 只留第一个。项目入口那一块的第一个换成入口(见 entries/entryMarks.ts)。webview 按这个画图标,不自己用种类查。 */ concepts: BlockConcept[] /** 能重写的那段注释的起止行,0 起。注释块是整块;函数、类等是它的说明。没有说明就没有,不显示重写按钮。 */ diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index ba488f9..62c7b33 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -744,6 +744,7 @@ export interface ThemeColor { dark: string, light: string } /** * 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 26 个给内置种类(几种共用一个: * 命令、管道、执行共用 square-terminal,解释器、解析指令共用 terminal,选项、设置共用 sliders-horizontal,字段、标签共用 tag), + * 宿主标的入口块也用这组里的 play(entries/entryMarks.ts 的 ENTRY_CONCEPT), * 后面的给库规则的种类(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出也在里面);库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标), * 没有按任意名字找图标的路。 */ @@ -838,7 +839,7 @@ export const KIND_ICONS: Record = { * 语言的种类不填颜色,图标按 id 用 CSS 里这种种类的颜色;规则声明的概念填颜色,只给这个图标上色。 */ export interface BlockConcept { - /** 稳定 id。语言的种类就是块的种类(class);规则的种类是 RuleCard.kind;规则另认的由规则起。 */ + /** 稳定 id。语言的种类就是块的种类(class);规则的种类是 RuleCard.kind;规则另认的由规则起;宿主标的入口是 entry。 */ id: string /** 悬停提示和读屏念的名字。 */ label: string diff --git a/vscode/src/codeReader/editor/documentMessage.test.ts b/vscode/src/codeReader/editor/documentMessage.test.ts index ce948d3..551e22d 100644 --- a/vscode/src/codeReader/editor/documentMessage.test.ts +++ b/vscode/src/codeReader/editor/documentMessage.test.ts @@ -4,6 +4,7 @@ import type { LibraryRule } from '../contract' import { describe, expect, it } from 'vitest' import { packageCommandRules } from '../commands/packageCommands' import { shellCommandRules } from '../commands/shellCommands' +import { ENTRY_CONCEPT } from '../entries/entryMarks' import { createBashAdapter } from '../languages/bash' import { createDockerfileAdapter } from '../languages/dockerfile' import { createPythonAdapter } from '../languages/python' @@ -217,26 +218,62 @@ describe('入口文件的预览', () => { ].join('\n') const nameRow = (block: BlockView): string => 'properties' in block ? propertyLines(block.properties)[0] : '' + const firstConcept = (block: BlockView): string => block.concepts[0].id - it('名字对上的块(变量、函数都算),名称那一行名字后面加浅色的「入口」', async () => { + it('名字对上的块(变量、函数都算):概念列表第一个换成入口,名称后面不加字', async () => { const { message } = await documentMessage({ ...doc('python', CLI), entryNames: ['app', 'main'] }, registryWithPython(), runtime, ignore) - expect(message.blocks.slice(1).map(nameRow)).toEqual(['名称: app 入口', '名称: main 入口', '名称: helper']) - const [, app] = message.blocks - expect('properties' in app && app.properties[0].value).toContainEqual({ text: ' 入口', role: 'hint' }) + const [, app, main, helper] = message.blocks + expect([app, main].map(block => block.concepts)).toEqual([[ENTRY_CONCEPT], [ENTRY_CONCEPT]]) + expect(firstConcept(helper)).toBe('function') + expect(message.blocks.slice(1).map(nameRow)).toEqual(['名称: app', '名称: main', '名称: helper']) + expect(ENTRY_CONCEPT).toEqual({ id: 'entry', label: '入口', icon: 'play', color: { dark: 'var(--vscode-testing-iconPassed)', light: 'var(--vscode-testing-iconPassed)' } }) }) - it('对不上名字、不是入口:什么都不加', async () => { + it('种类、筛选属性、属性表都不变,只换概念列表', async () => { + const plain = await documentMessage(doc('python', CLI), registryWithPython(), runtime, ignore) + const { message } = await documentMessage({ ...doc('python', CLI), entryNames: ['main'] }, registryWithPython(), runtime, ignore) + const { concepts: _marked, ...main } = message.blocks[2] + const { concepts: _plain, ...before } = plain.message.blocks[2] + expect(main).toEqual(before) + expect(message.attributes).toEqual(plain.message.attributes) + }) + + it('库规则的卡片画几个概念图标:入口替掉第一个(种类),后面的照旧', async () => { + const API = [ + 'from fastapi import FastAPI', + '', + 'app = FastAPI()', + '', + '', + '@app.get("/")', + 'def main():', + ' return {}', + '', + ].join('\n') + const plain = await documentMessage(doc('python', API), registryWithPython(fastapiRule), runtime, ignore) + const { message } = await documentMessage({ ...doc('python', API), entryNames: ['main'] }, registryWithPython(fastapiRule), runtime, ignore) + const before = plain.message.blocks.at(-1)! + const after = message.blocks.at(-1)! + expect(before.badge).toBeDefined() + expect(before.concepts.length).toBeGreaterThan(1) + expect(after.concepts).toEqual([ENTRY_CONCEPT, ...before.concepts.slice(1)]) + expect(after.badge).toEqual(before.badge) + expect(after.kind).toBe(before.kind) + }) + + it('对不上名字、不是入口:什么都不改', async () => { const plain = await documentMessage(doc('python', CLI), registryWithPython(), runtime, ignore) const unmatched = await documentMessage({ ...doc('python', CLI), entryNames: ['run'] }, registryWithPython(), runtime, ignore) expect(unmatched.message.blocks).toEqual(plain.message.blocks) - expect(plain.message.blocks.map(nameRow).join('\n')).not.toContain('入口') + expect(plain.message.blocks.map(firstConcept)).not.toContain('entry') }) - it('rust:fn main 的名称后面加「入口」', async () => { + it('rust:fn main 的概念列表第一个是入口', async () => { const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick) registry.registerLanguage(createRustAdapter(WASM_DIR)) registry.registerTranslator(rustZhTranslator) const { message } = await documentMessage({ ...doc('rust', 'fn main() {\n run();\n}\n\nfn run() {}\n'), entryNames: ['main'] }, registry, runtime, ignore) - expect(message.blocks.map(nameRow)).toEqual(['名称: main 入口', '名称: run']) + expect(message.blocks.map(firstConcept)).toEqual(['entry', 'function']) + expect(message.blocks.map(nameRow)).toEqual(['名称: main', '名称: run']) }) }) diff --git a/vscode/src/codeReader/editor/documentMessage.ts b/vscode/src/codeReader/editor/documentMessage.ts index 474ee52..938ce75 100644 --- a/vscode/src/codeReader/editor/documentMessage.ts +++ b/vscode/src/codeReader/editor/documentMessage.ts @@ -5,7 +5,7 @@ import type { DocumentMessage } from './messages' import { fileAttributes, mergeAttributeNames } from '../attributes' import { readBlockViews } from '../blockView' import { commandRuleLookup } from '../commandRules' -import { markEntryNames } from '../entries/entryMarks' +import { markEntryBlocks } from '../entries/entryMarks' import { ruleAttributeNames } from '../libraryRules' /** 生成预览数据要读的那点文档信息(TextDocument 的子集)。 */ @@ -13,7 +13,7 @@ export interface DocumentSnapshot { languageId: string version: number getText: () => string - /** 这份文件是项目写明的入口时,预览里要标「入口」的名字(见 entries/entryPoints.ts) */ + /** 这份文件是项目写明的入口时,预览里名字对上的块标成入口(见 entries/entryPoints.ts) */ entryNames?: readonly string[] } @@ -28,7 +28,7 @@ export interface DocumentRead { * 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下; * 规则查名字的定义时问 lookup(RuleContext.lookup),翻译插件问调用的名字定义成哪种块时问 calleeKind(SourceDocument.calleeKind)。 * 筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。 - * 入口文件里名字对上的块,名称那一行在翻译好的结果上加「入口」(markEntryNames)。 + * 入口文件里名字对上的块,概念列表的第一个换成入口(markEntryBlocks)。 */ export async function documentMessage( document: DocumentSnapshot, @@ -70,7 +70,7 @@ export async function documentMessage( }, }) const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)]) - return { message: { ...message, blocks: markEntryNames(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols } + return { message: { ...message, blocks: markEntryBlocks(blocks, source, document.entryNames ?? []), attributes: fileAttributes(blocks, names) }, displayNames, symbols } } catch (err) { return { message: { ...message, error: err instanceof Error ? err.message : String(err) }, displayNames: [], symbols: [] } diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index acd7c2d..8aab798 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -77,7 +77,7 @@ export class PreviewEditorProvider implements CustomTextEditorProvider { readonly indexed: (index: WorkspaceIndex, key: string) => Promise, /** 预览标差异时对比什么 */ readonly diffBases: DiffBases, - /** 哪些文件是项目写明的入口、预览里标哪些名字 */ + /** 哪些文件是项目写明的入口、预览里哪些名字的块标成入口 */ readonly entries: Pick, ) {} @@ -256,7 +256,7 @@ class Preview { if (this.ready) this.post({ type: 'filter', ...state }) }), - // 项目的入口变了:重新发文档,名称那一行的「入口」跟着变 + // 项目的入口变了:重新发文档,卡片左边的入口图标跟着变 owner.entries.onDidChange((uris) => { if (uris.some(uri => uri.toString() === document.uri.toString())) this.schedulePush() @@ -352,7 +352,7 @@ class Preview { /** * 对比基准里的旧版本排成视图数据:和这份文档同一种语言、同一个翻译插件和库规则;规则查定义、调用的种类用这份文档眼下查到的答案, - * 两边同样的写法翻得一样,不会因为答案晚到被当成改了;项目入口也按眼下的标,「入口」两边都有。旧版本里规则出的错不记(记的是现在这份的)。 + * 两边同样的写法翻得一样,不会因为答案晚到被当成改了;项目入口也按眼下的标,两边的入口块一样。旧版本里规则出的错不记(记的是现在这份的)。 */ private async readOld(source: string): Promise { const { message } = await documentMessage( diff --git a/vscode/src/codeReader/entries/entryMarks.ts b/vscode/src/codeReader/entries/entryMarks.ts index cf6184d..4edc1ff 100644 --- a/vscode/src/codeReader/entries/entryMarks.ts +++ b/vscode/src/codeReader/entries/entryMarks.ts @@ -1,31 +1,26 @@ import type { BlockView } from '../blockView' -import type { Segment } from '../contract' +import type { BlockConcept } from '../contract' -/** 名称那一行、名字后面的浅色说明词。 */ -const ENTRY_HINT: Segment = { text: ' 入口', role: 'hint' } +/** 入口这个概念:绿色播放图标,绿色就是侧栏入口文件图标的主题色 `testing.iconPassed`(webview 里是同名的 CSS 变量,跟着主题变)。 */ +export const ENTRY_CONCEPT: BlockConcept = { + id: 'entry', + label: '入口', + icon: 'play', + color: { dark: 'var(--vscode-testing-iconPassed)', light: 'var(--vscode-testing-iconPassed)' }, +} /** * 入口文件的预览:文件最外层、名字是入口名字之一的那一块(函数、常量、变量、库规则的卡片,谁对上就是谁), - * 属性表里写着这个名字的那一行、名字后面加一个浅色的「入口」。和语言、翻译插件无关: - * 块的名字按它定义处(defines)在源码里的那一段认,属性表里按词的源码下标(offset)找写名字的那个词;对不上就什么都不加。 + * 概念列表的第一个(种类)换成入口(ENTRY_CONCEPT),后面别的概念照旧;种类、筛选、卡片边框都不变。和语言、翻译插件无关: + * 块的名字按它定义处(defines)在源码里的那一段认;对不上就什么都不改。 */ -export function markEntryNames(blocks: BlockView[], source: string, names: readonly string[]): BlockView[] { +export function markEntryBlocks(blocks: BlockView[], source: string, names: readonly string[]): BlockView[] { if (names.length === 0) return blocks return blocks.map((block) => { const at = block.defines - if (!at || !('properties' in block) || !names.includes(source.slice(at.startIndex, at.endIndex))) + if (!at || !names.includes(source.slice(at.startIndex, at.endIndex))) return block - let marked = false - const properties = block.properties.map((row) => { - if (marked || 'markdown' in row.value) - return row - const index = row.value.findIndex(piece => 'role' in piece && piece.offset === at.startIndex) - if (index < 0) - return row - marked = true - return { ...row, value: [...row.value.slice(0, index + 1), ENTRY_HINT, ...row.value.slice(index + 1)] } - }) - return marked ? { ...block, properties } : block + return { ...block, concepts: [ENTRY_CONCEPT, ...block.concepts.slice(1)] } }) } diff --git a/vscode/src/codeReader/explorer/rowDecoration.test.ts b/vscode/src/codeReader/explorer/rowDecoration.test.ts index 0b66aaf..2da5527 100644 --- a/vscode/src/codeReader/explorer/rowDecoration.test.ts +++ b/vscode/src/codeReader/explorer/rowDecoration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { escapeMarkdown, rowDecoration } from './rowDecoration' +import { ENTRY_ICON, escapeMarkdown, rowDecoration } from './rowDecoration' describe('树上的一行', () => { it('没有注释、不是入口的行保持原样', () => { @@ -15,21 +15,27 @@ describe('树上的一行', () => { expect(rowDecoration('folder', 'campaign', '营销活动', undefined)?.contextValue).toBe('folder noted') }) - it('入口、没注释:灰字是「入口」,提示里写从哪个名字开始读,contextValue 不变', () => { + it('入口、没注释:没有灰字,图标换成绿色播放,提示里写从哪个名字开始读,contextValue 不变', () => { expect(rowDecoration('file', 'src/agent_cli/main.py', undefined, ['app', 'dev'])).toEqual({ - description: '入口', tooltip: '**src\\/agent\\_cli\\/main\\.py** \n入口,从 `app`、`dev` 开始读', contextValue: 'file', + icon: { id: 'play', color: 'testing.iconPassed' }, }) expect(rowDecoration('file', 'bin/cli.js', undefined, [])?.tooltip).toBe('**bin\\/cli\\.js** \n入口') }) - it('入口、有注释:灰字是「注释 · 入口」,注释不被盖掉,提示里注释下面再写入口', () => { + it('入口、有注释:灰字只是注释,图标换成绿色播放,提示里注释下面再写入口', () => { expect(rowDecoration('file', 'src/main.rs', '服务启动', ['main'])).toEqual({ - description: '服务启动 · 入口', + description: '服务启动', tooltip: '**src\\/main\\.rs** \n服务启动 \n入口,从 `main` 开始读', contextValue: 'file noted', + icon: ENTRY_ICON, }) + expect(ENTRY_ICON).toEqual({ id: 'play', color: 'testing.iconPassed' }) + }) + + it('不是入口的行不换图标', () => { + expect(rowDecoration('file', 'src/lib.rs', '库', undefined)?.icon).toBeUndefined() }) it('注释里的 Markdown 符号原样显示', () => { diff --git a/vscode/src/codeReader/explorer/rowDecoration.ts b/vscode/src/codeReader/explorer/rowDecoration.ts index ae00e36..a513822 100644 --- a/vscode/src/codeReader/explorer/rowDecoration.ts +++ b/vscode/src/codeReader/explorer/rowDecoration.ts @@ -1,11 +1,22 @@ -/** 侧栏一行名字后面的灰字和悬停提示:文件注释、项目入口。 */ +/** 侧栏一行换上的图标:VS Code 自带图标字体里的名字(ThemeIcon 的 id)和主题色的键(ThemeColor 的 id)。 */ +export interface RowIcon { + id: string + color: string +} + +/** 入口文件的图标:绿色播放,绿色是测试通过的那种(主题色 `testing.iconPassed`)。 */ +export const ENTRY_ICON: RowIcon = { id: 'play', color: 'testing.iconPassed' } + +/** 侧栏一行名字后面的灰字、悬停提示和图标:文件注释、项目入口。 */ export interface RowDecoration { - /** 名字后面的灰字:注释,是入口时后面接「 · 入口」,没注释时就是「入口」;VS Code 放不下时自己截断 */ - description: string + /** 名字后面的灰字:文件注释,没注释时没有;VS Code 放不下时自己截断 */ + description?: string /** 悬停提示(Markdown):第一行粗体的相对路径,下面是注释全文,是入口时再一行写明 */ tooltip: string /** 有注释时末尾加上 ` noted`:菜单的 when 子句靠它在「添加注释」和「编辑注释」之间换;是不是入口不影响 */ contextValue: string + /** 是入口时文件图标换成 ENTRY_ICON;git 改动的颜色和字母照旧由 VS Code 按文件加 */ + icon?: RowIcon } /** 每个 ASCII 标点前加反斜杠(CommonMark 允许任何 ASCII 标点这样转义),注释里的 `*`、`_`、`<`、`[` 等原样显示。 */ @@ -29,8 +40,9 @@ export function rowDecoration(contextValue: string, key: string, note: string | if (entry !== undefined) lines.push(entry.length > 0 ? `入口,从 ${entry.map(name => `\`${name}\``).join('、')} 开始读` : '入口') return { - description: note === undefined ? '入口' : entry === undefined ? note : `${note} · 入口`, + ...(note !== undefined && { description: note }), tooltip: lines.join(' \n'), contextValue: note === undefined ? contextValue : `${contextValue} noted`, + ...(entry !== undefined && { icon: ENTRY_ICON }), } } diff --git a/vscode/src/codeReader/explorer/rowDecorator.ts b/vscode/src/codeReader/explorer/rowDecorator.ts index f23cdec..523b7d0 100644 --- a/vscode/src/codeReader/explorer/rowDecorator.ts +++ b/vscode/src/codeReader/explorer/rowDecorator.ts @@ -1,6 +1,6 @@ import type { Event, TreeItem, Uri } from 'vscode' import type { FileNode, FileTreeDecorator } from './fileTree' -import { Disposable, MarkdownString } from 'vscode' +import { Disposable, MarkdownString, ThemeColor, ThemeIcon } from 'vscode' import { locate } from '../folderStore' import { rowDecoration } from './rowDecoration' @@ -16,7 +16,10 @@ export interface RowEntries { namesOf: (uri: Uri) => readonly string[] | undefined } -/** 侧栏一行名字后面的灰字和悬停提示:文件注释,后面接项目入口(见 rowDecoration);工作区外面的行(审查的工作副本)不加。 */ +/** + * 侧栏一行名字后面的灰字(文件注释)、悬停提示,入口文件换成绿色播放图标(见 rowDecoration);工作区外面的行(审查的工作副本)不加。 + * 只改这棵树自己的行,资源管理器、标签页不受影响。 + */ export class RowDecorator implements FileTreeDecorator { constructor(private readonly notes: RowNotes, private readonly entries: RowEntries) {} @@ -33,8 +36,11 @@ export class RowDecorator implements FileTreeDecorator { const decoration = at && rowDecoration(item.contextValue ?? '', at.key, this.notes.noteOf(node), entry) if (!decoration) return - item.description = decoration.description + if (decoration.description !== undefined) + item.description = decoration.description item.tooltip = new MarkdownString(decoration.tooltip) item.contextValue = decoration.contextValue + if (decoration.icon) + item.iconPath = new ThemeIcon(decoration.icon.id, new ThemeColor(decoration.icon.color)) } }