✨ feat(vscode): 代码阅读的入口文件和入口块换成绿色播放图标,不再写「入口」二字

This commit is contained in:
cruldra committed 2026-10-08 07:17:25 +08:00
1 parent 5827defd51
commit f6bc217e91
10 files changed
+110 -53

No files matched your search

+6 -6
View File
@@ -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 的形状、每条怎么认、记一次 / 删一次、再问时接着哪个会话(纯函数)
+1 -1
View File
@@ -33,7 +33,7 @@ interface BlockViewBase {
attributes: BlockAttributes
/**
* 这块认出来的概念,从左到右:语言的种类(再加语言插件另认的),库规则的卡片再加规则的种类和规则另认的。
* 同一个 id 只留第一个。webview 按这个画图标,不自己用种类查。
* 同一个 id 只留第一个。项目入口那一块的第一个换成入口(见 entries/entryMarks.ts)。webview 按这个画图标,不自己用种类查。
*/
concepts: BlockConcept[]
/** 能重写的那段注释的起止行,0 起。注释块是整块;函数、类等是它的说明。没有说明就没有,不显示重写按钮。 */
+2 -1
View File
@@ -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<BlockKind, KindIconName> = {
* 语言的种类不填颜色,图标按 id 用 CSS 里这种种类的颜色;规则声明的概念填颜色,只给这个图标上色。
*/
export interface BlockConcept {
/** 稳定 id。语言的种类就是块的种类(class);规则的种类是 RuleCard.kind;规则另认的由规则起。 */
/** 稳定 id。语言的种类就是块的种类(class);规则的种类是 RuleCard.kind;规则另认的由规则起;宿主标的入口是 entry。 */
id: string
/** 悬停提示和读屏念的名字。 */
label: string
@@ -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<Memento, 'get' | 'update'>)
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'])
})
})
@@ -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: [] }
@@ -77,7 +77,7 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
readonly indexed: (index: WorkspaceIndex, key: string) => Promise<IndexedEntry[]>,
/** 预览标差异时对比什么 */
readonly diffBases: DiffBases,
/** 哪些文件是项目写明的入口、预览里标哪些名字 */
/** 哪些文件是项目写明的入口、预览里哪些名字的块标成入口 */
readonly entries: Pick<WorkspaceEntries, 'namesOf' | 'onDidChange'>,
) {}
@@ -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<OldDocument | undefined> {
const { message } = await documentMessage(
+13 -18
View File
@@ -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)] }
})
}
@@ -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 符号原样显示', () => {
@@ -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 }),
}
}
@@ -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))
}
}