✨ feat(vscode): 代码阅读卡片横排多个概念图标
This commit is contained in:
1 parent
11fcbef225
commit
394cd2fc1c
19 files changed
+328
-99
No files matched your search
@@ -108,7 +108,7 @@ describe('块的筛选属性:宿主从 kind 和 facts 推出通用属性', ()
|
||||
})
|
||||
|
||||
function view(id: string, attributes: Record<string, string>, children: BlockView[] = []): BlockView {
|
||||
return { id, anchor: id, kind: 'statement', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes, children, translation: [] }
|
||||
return { id, anchor: id, kind: 'statement', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes, concepts: [{ id: 'statement', label: '语句', icon: 'code' }], children, translation: [] }
|
||||
}
|
||||
|
||||
describe('文件里出现过的属性', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { LanguageAdapter, Translator } from './contract'
|
||||
import type { LanguageAdapter, LibraryRule, Translator } from './contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readBlockViews, readDefinitionKind, toBlockViews } from './blockView'
|
||||
import { replaceLines } from './editText'
|
||||
@@ -333,6 +333,48 @@ describe('blockView', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('概念列表:语言的种类在前,语言另认的接着,规则的种类和另认的在后;同一个 id 只留第一个', async () => {
|
||||
const language: LanguageAdapter = {
|
||||
...py,
|
||||
concepts: block => block.kind === 'class'
|
||||
? [
|
||||
{ id: 'class', label: '重复的类', icon: 'zap' },
|
||||
{ id: 'python.abstract', label: '抽象', icon: 'braces' },
|
||||
]
|
||||
: [],
|
||||
}
|
||||
const rule: LibraryRule = {
|
||||
id: 'demo',
|
||||
label: '演示',
|
||||
languageIds: ['python'],
|
||||
libraries: [],
|
||||
claim: block => block.kind === 'class'
|
||||
? {
|
||||
kind: 'demo.thing',
|
||||
badge: { label: '东西', color: { dark: '#111111', light: '#222222' }, icon: 'package' },
|
||||
rows: [],
|
||||
absorbs: [],
|
||||
concepts: [
|
||||
{ id: 'class', label: '又一个类', icon: 'lock' },
|
||||
{ id: 'demo.thing', label: '重复的东西', icon: 'tag', color: { dark: '#333333', light: '#444444' } },
|
||||
{ id: 'demo.extra', label: '额外', icon: 'lock', color: { dark: '#555555', light: '#666666' } },
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
const source = 'class Box:\n pass\n'
|
||||
const [view] = await withBlocks(language, 'python', source, blocks =>
|
||||
toBlockViews(blocks, { source, language, markers: [] }, pythonZhTranslator, { rules: [rule], report: () => {} }))
|
||||
expect(view?.concepts).toEqual([
|
||||
{ id: 'class', label: '类', icon: 'box' },
|
||||
{ id: 'python.abstract', label: '抽象', icon: 'braces' },
|
||||
{ id: 'demo.thing', label: '东西', icon: 'package', color: { dark: '#111111', light: '#222222' } },
|
||||
{ id: 'demo.extra', label: '额外', icon: 'lock', color: { dark: '#555555', light: '#666666' } },
|
||||
])
|
||||
expect(view?.kind).toBe('demo.thing')
|
||||
expect(view?.attributes.kind).toBe('demo.thing')
|
||||
})
|
||||
|
||||
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))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Tree } from '@vscode/tree-sitter-wasm'
|
||||
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
|
||||
import type { BlockAttributes, BlockConcept, BlockKind, CodeBlock, DisplayName, EmbeddedCode, EmbeddedReading, Inline, KindBadge, LanguageAdapter, LibraryRule, LocalSymbol, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleCard, RuleContext, Segment, SourceDocument, SourceRange, SourceSpan, StepBlocks, TextBlock, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { KIND_ICONS } from './contract'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
import { activeRules, claimBlock, importsIn } from './libraryRules'
|
||||
import { BlockAnchors } from './qa/anchor'
|
||||
@@ -29,7 +30,12 @@ interface BlockViewBase {
|
||||
defines?: SourceRange
|
||||
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
|
||||
attributes: BlockAttributes
|
||||
/** 库规则的卡片才有:种类的名字、颜色和图标(RuleCard.badge)。预览同类块一样默认收起它的子块。 */
|
||||
/**
|
||||
* 这块认出来的概念,从左到右:语言的种类(再加语言插件另认的),库规则的卡片再加规则的种类和规则另认的。
|
||||
* 同一个 id 只留第一个。webview 按这个画图标,不自己用种类查。
|
||||
*/
|
||||
concepts: BlockConcept[]
|
||||
/** 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色(RuleCard.badge)。图标在 concepts 里。预览同类块一样默认收起它的子块。 */
|
||||
badge?: KindBadge
|
||||
/** 库规则放进父卡片里的子块才有(见 RuleSlot):画在父卡片属性表第 row 行下面,不排在父卡片的子块里。 */
|
||||
slot?: BlockSlot
|
||||
@@ -297,6 +303,33 @@ export function toBlockViews(
|
||||
const [only] = steps
|
||||
return layer(steps, part.document, part.translator, NO_RULES)(steps.length === 1 && only ? only.children : steps, `${id}.`, scope, from)
|
||||
}
|
||||
/**
|
||||
* 这块的概念,从左到右,同一个 id 只留第一个:语言的种类(图标、名字用内置种类和翻译插件起的种类名),
|
||||
* 语言插件另认的,库规则的卡片再加规则的种类和规则另认的。card 不填就是没有规则卡片(认成现成块的也算)。
|
||||
*/
|
||||
function blockConcepts(block: CodeBlock, card?: RuleCard): BlockConcept[] {
|
||||
const seen = new Set<string>()
|
||||
const concepts: BlockConcept[] = []
|
||||
const add = (concept: BlockConcept): void => {
|
||||
if (seen.has(concept.id))
|
||||
return
|
||||
seen.add(concept.id)
|
||||
concepts.push(concept)
|
||||
}
|
||||
add({
|
||||
id: block.kind,
|
||||
label: layerTranslator?.attributeNames?.values.kind?.[block.kind] ?? block.kind,
|
||||
icon: KIND_ICONS[block.kind],
|
||||
})
|
||||
for (const concept of layerDocument.language.concepts?.(block) ?? [])
|
||||
add(concept)
|
||||
if (card) {
|
||||
add({ id: card.kind, label: card.badge.label, icon: card.badge.icon, color: card.badge.color })
|
||||
for (const concept of card.concepts ?? [])
|
||||
add(concept)
|
||||
}
|
||||
return concepts
|
||||
}
|
||||
/** 块照它自己的样子(语言插件报的,或库规则认成的属性表块):编辑范围是 span,头部是块自己那部分,子块照常交给各规则看。scope 见 BlockAnchors.place */
|
||||
function plain(block: CodeBlock, span: SourceSpan, id: string, scope: string): BlockView {
|
||||
const name = ownName(block)
|
||||
@@ -309,6 +342,7 @@ export function toBlockViews(
|
||||
headerEndLine: block.header.endRow,
|
||||
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
|
||||
attributes: blockAttributes(block),
|
||||
concepts: blockConcepts(block),
|
||||
children: [
|
||||
...list(block.children, `${id}.`, inner),
|
||||
...embeddedChildren(block, id, inner, block.children.length),
|
||||
@@ -343,6 +377,7 @@ export function toBlockViews(
|
||||
headerEndLine: span.endRow,
|
||||
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
|
||||
attributes: { ...blockAttributes(block), ...attributes, kind },
|
||||
concepts: blockConcepts(block, replacement),
|
||||
badge,
|
||||
children: list(kept, `${id}.`, inner, 0, slots),
|
||||
properties: rows,
|
||||
|
||||
@@ -652,6 +652,11 @@ export interface LanguageAdapter {
|
||||
steps?: (root: Node, source: string, languageId: string) => StepBlocks
|
||||
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
|
||||
displayNames?: DisplayNameReader
|
||||
/**
|
||||
* 这块除了自己的种类,还认得出的概念(异步、抽象)。认到才返回;不填就只有种类那一个。
|
||||
* 种类那一个由宿主按内置种类的图标和翻译插件起的种类名补在最前面,这里不要再报种类。
|
||||
*/
|
||||
concepts?: (block: CodeBlock) => BlockConcept[]
|
||||
}
|
||||
|
||||
// ── 中文名(见「中文名」)──
|
||||
@@ -777,7 +782,59 @@ export const KIND_ICON_NAMES = [
|
||||
|
||||
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
|
||||
|
||||
/** 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。 */
|
||||
/**
|
||||
* 内置种类用哪个图标。宿主排预览时写进概念列表,webview 只按名字画。
|
||||
* 同一类语义共用一个颜色(codeReader.css 的 --cr-kind-*:常量 / 变量 / 字段都是数据,尝试 / 捕获都是异常处理),图标区分具体是哪种。
|
||||
*/
|
||||
export const KIND_ICONS: Record<BlockKind, KindIconName> = {
|
||||
import: 'import',
|
||||
function: 'square-function',
|
||||
class: 'box',
|
||||
type: 'braces',
|
||||
enum: 'list-ordered',
|
||||
exception: 'triangle-alert',
|
||||
field: 'tag',
|
||||
const: 'lock',
|
||||
variable: 'variable',
|
||||
if: 'split',
|
||||
else: 'corner-down-right',
|
||||
loop: 'repeat',
|
||||
try: 'shield',
|
||||
catch: 'shield-alert',
|
||||
finally: 'shield-check',
|
||||
return: 'corner-down-left',
|
||||
statement: 'code',
|
||||
comment: 'message-square-text',
|
||||
interpreter: 'terminal',
|
||||
options: 'sliders-horizontal',
|
||||
case: 'signpost',
|
||||
command: 'square-terminal',
|
||||
pipeline: 'square-terminal',
|
||||
directive: 'terminal',
|
||||
stage: 'layers',
|
||||
run: 'square-terminal',
|
||||
copy: 'copy',
|
||||
setting: 'sliders-horizontal',
|
||||
label: 'tag',
|
||||
health: 'activity',
|
||||
start: 'play',
|
||||
}
|
||||
|
||||
/**
|
||||
* 一块上认出的一个概念:稳定 id、悬停名、图标名(只能从 KIND_ICON_NAMES 里挑)、颜色。
|
||||
* 语言的种类不填颜色,图标按 id 用 CSS 里这种种类的颜色;规则声明的概念填颜色,只给这个图标上色。
|
||||
*/
|
||||
export interface BlockConcept {
|
||||
/** 稳定 id。语言的种类就是块的种类(class);规则的种类是 RuleCard.kind;规则另认的由规则起。 */
|
||||
id: string
|
||||
/** 悬停提示和读屏念的名字。 */
|
||||
label: string
|
||||
icon: KindIconName
|
||||
/** 不填是语言的种类(颜色在 CSS)。规则的概念填规则给的颜色,不改卡片边框。 */
|
||||
color?: ThemeColor
|
||||
}
|
||||
|
||||
/** 库规则的种类:筛选栏里叫 label,卡片边框用 color,icon 进概念列表。卡片左边不只这一个图标(见 BlockConcept)。 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
/**
|
||||
@@ -793,12 +850,17 @@ export type RuleBlock = RuleCard | RuleFacts | RuleOmit
|
||||
*/
|
||||
export interface RuleOmit { omit: true }
|
||||
|
||||
/** 规则自己的卡片:一张属性表,种类的图标、颜色、名字和每一行都由规则给(用词归规则)。 */
|
||||
/** 规则自己的卡片:一张属性表。种类(badge)决定筛选和边框色;卡片左边的图标是语言的概念再加这个种类、以及 concepts。用词归规则。 */
|
||||
export interface RuleCard {
|
||||
/** 种类 id,写成「规则 id.名字」(sqlalchemy.table):筛选的 kind 属性值就是它,筛选记住的也是它,要稳定。 */
|
||||
kind: string
|
||||
/** 种类的名字、颜色和图标:筛选栏里这个种类也叫这个名字。 */
|
||||
/** 种类的名字、颜色和图标:筛选栏里这个种类也叫这个名字,卡片边框用这个颜色。图标进概念列表,不换掉语言的概念。 */
|
||||
badge: KindBadge
|
||||
/**
|
||||
* 认领时额外认出的概念,排在这个种类(badge)后面。语言认的概念仍在更前面;同一个 id 只留第一个。
|
||||
* 不改筛选的种类,也不改卡片边框色(仍是 badge)。
|
||||
*/
|
||||
concepts?: BlockConcept[]
|
||||
rows: PropertyRow[]
|
||||
/** 块自己的名字(类名)在源码里的范围:跳到的定义就是它时闪整块。 */
|
||||
defines?: SourceRange
|
||||
|
||||
@@ -20,7 +20,7 @@ function textView(translation: Inline[] | { markdown: string }, extra: Partial<B
|
||||
}
|
||||
|
||||
function propertyView(properties: PropertyRow[]): BlockView {
|
||||
return { id: '0', anchor: 'a', kind: 'function', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes: {}, children: [], properties }
|
||||
return { id: '0', anchor: 'a', kind: 'function', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes: {}, concepts: [{ id: 'function', label: '函数', icon: 'square-function' }], children: [], properties }
|
||||
}
|
||||
|
||||
describe('inlineDiff', () => {
|
||||
|
||||
@@ -135,6 +135,43 @@ describe('dataclasses 库规则(Python)', () => {
|
||||
expect(sandbox.badge).toEqual({ label: '数据类', color: { dark: '#ef7fb0', light: '#b3306c' }, icon: 'package' })
|
||||
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
|
||||
.toContainEqual({ id: 'dataclasses.dataclass', name: '数据类', count: 4 })
|
||||
expect(sandbox.concepts.map(concept => concept.label)).toEqual(['类', '数据类', '不可修改'])
|
||||
})
|
||||
|
||||
it('概念图标:普通类只有「类」;数据类是「类」和「数据类」,frozen 再加一把锁;slots 不加;悬停名是这些', async () => {
|
||||
const list = await views([
|
||||
'class Plain:',
|
||||
' x = 1',
|
||||
'',
|
||||
'from dataclasses import dataclass',
|
||||
'',
|
||||
'@dataclass',
|
||||
'class Open:',
|
||||
' x: int',
|
||||
'',
|
||||
'@dataclass(frozen=False)',
|
||||
'class Soft:',
|
||||
' x: int',
|
||||
'',
|
||||
'@dataclass(frozen=True)',
|
||||
'class Closed:',
|
||||
' x: int',
|
||||
'',
|
||||
'@dataclass(frozen=True, slots=True)',
|
||||
'class Packed:',
|
||||
' x: int',
|
||||
'',
|
||||
].join('\n'))
|
||||
const plain = list.find(view => view.kind === 'class')
|
||||
expect(plain?.concepts).toEqual([{ id: 'class', label: '类', icon: 'box' }])
|
||||
const dataClass = { id: 'dataclasses.dataclass', label: '数据类', icon: 'package' as const, color: { dark: '#ef7fb0', light: '#b3306c' } }
|
||||
const frozen = { id: 'dataclasses.frozen', label: '不可修改', icon: 'lock' as const, color: { dark: '#9cdcfe', light: '#001080' } }
|
||||
expect(named(list, 'Open').concepts).toEqual([{ id: 'class', label: '类', icon: 'box' }, dataClass])
|
||||
expect(named(list, 'Soft').concepts).toEqual([{ id: 'class', label: '类', icon: 'box' }, dataClass])
|
||||
expect(named(list, 'Closed').concepts).toEqual([{ id: 'class', label: '类', icon: 'box' }, dataClass, frozen])
|
||||
expect(named(list, 'Packed').concepts).toEqual([{ id: 'class', label: '类', icon: 'box' }, dataClass, frozen])
|
||||
expect(named(list, 'Closed').attributes.kind).toBe('dataclasses.dataclass')
|
||||
expect(named(list, 'Closed').badge?.color).toEqual(dataClass.color)
|
||||
})
|
||||
|
||||
it('编辑范围是整个类,每行只在一块里能编辑;改卡片写回整个类', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import type { Access, BlockConcept, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import type { Call, LibraryNames } from './pythonTree'
|
||||
import { memberDocs, withLeadingComments } from './children'
|
||||
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
||||
@@ -17,6 +17,8 @@ const TYPE_LIBRARIES = ['dataclasses', 'typing', 'typing_extensions']
|
||||
const KIND = 'dataclasses.dataclass'
|
||||
const COLOR: ThemeColor = { dark: '#ef7fb0', light: '#b3306c' }
|
||||
const ICON: KindIconName = 'package'
|
||||
/** frozen=True 的锁,颜色同常量:一眼是「不可修改」。 */
|
||||
const FROZEN_COLOR: ThemeColor = { dark: '#9cdcfe', light: '#001080' }
|
||||
|
||||
/** `@dataclass(...)` 各参数不写时的值:写成这个值等于没写,「选项」里不讲。 */
|
||||
const OPTION_DEFAULTS: Record<string, boolean> = {
|
||||
@@ -238,6 +240,13 @@ function fieldItem(field: Field, children: readonly CodeBlock[], env: Env): Prop
|
||||
}
|
||||
}
|
||||
|
||||
/** frozen=True 才算一个概念(锁,悬停「不可修改」)。slots 没有一眼能看懂的图标,不加。 */
|
||||
function frozenConcept(call: Call | undefined, words: Words): BlockConcept[] {
|
||||
if (call?.keywords.get('frozen')?.type !== 'true')
|
||||
return []
|
||||
return [{ id: 'dataclasses.frozen', label: words.options['frozen=True'], icon: 'lock', color: FROZEN_COLOR }]
|
||||
}
|
||||
|
||||
function dataclassCard(block: ClassBlock, decorator: Node, call: Call | undefined, env: Env): RuleCard {
|
||||
const { words, context } = env
|
||||
const { labels } = words
|
||||
@@ -260,9 +269,11 @@ function dataclassCard(block: ClassBlock, decorator: Node, call: Call | undefine
|
||||
items: fields.map(field => fieldItem(field, children, env)),
|
||||
},
|
||||
]
|
||||
const concepts = frozenConcept(call, words)
|
||||
return {
|
||||
kind: KIND,
|
||||
badge: { label: words.kind, color: COLOR, icon: ICON },
|
||||
...(concepts.length > 0 && { concepts }),
|
||||
rows,
|
||||
...(facts.name && { defines: range(facts.name) }),
|
||||
absorbs: withLeadingComments(children, [...fields.map(field => field.block), ...markers]),
|
||||
|
||||
Reference in new issue
Block a user