✨ feat(vscode): 代码阅读的入口文件和入口块换成绿色播放图标,不再写「入口」二字
This commit is contained in:
1 parent
5827defd51
commit
f6bc217e91
10 files changed
+110
-53
No files matched your search
@@ -33,7 +33,7 @@ interface BlockViewBase {
|
||||
attributes: BlockAttributes
|
||||
/**
|
||||
* 这块认出来的概念,从左到右:语言的种类(再加语言插件另认的),库规则的卡片再加规则的种类和规则另认的。
|
||||
* 同一个 id 只留第一个。webview 按这个画图标,不自己用种类查。
|
||||
* 同一个 id 只留第一个。项目入口那一块的第一个换成入口(见 entries/entryMarks.ts)。webview 按这个画图标,不自己用种类查。
|
||||
*/
|
||||
concepts: BlockConcept[]
|
||||
/** 能重写的那段注释的起止行,0 起。注释块是整块;函数、类等是它的说明。没有说明就没有,不显示重写按钮。 */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user