✨ feat(vscode): 代码阅读预览里跨几行写的列表按项拆开,列表里整项是调用的带着自己的实参表,一层层能往下套

This commit is contained in:
cruldra committed 2026-10-13 13:32:45 +11:00
1 parent 7d6e7592c9
commit 2c490cac2d
22 files changed
+848 -314

No files matched your search

+51 -18
View File
@@ -304,33 +304,66 @@ describe('python 属性表信息(facts)', () => {
expect(found[10]).not.toHaveProperty('access')
})
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
const found = await facts('EMPTY = {}\nITEMS = [1]\nclass K:\n labels = {"x": 1}')
expect(found[1]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}' }] })
expect(found[2]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[4]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}' }] })
})
})
describe('python 字面量的各项', () => {
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.collection?.(node))
it('字典:字符串键、名字键、**展开,注释不算一项;值里的字典另有自己的各项,空字典没有项', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}, "b": [1]},',
' "nested": {"a": {}},',
' **other,',
'}',
'EMPTY = {}',
'ITEMS = [1]',
'class K:',
' labels = {"x": 1}',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private', declarators: [{ name: '_KEYS', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}, "b": [1]}', entries: [
{ kind: 'pair', key: '"a"', value: '{}', entries: [] },
{ kind: 'pair', key: '"b"', value: '[1]' },
expect(await collections(source)).toEqual([
{ kind: 'map', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}}' },
{ kind: 'spread', value: 'other' },
] },
{ kind: 'spread', value: 'other' },
] }] })
expect(found[8]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}', entries: [] }] })
expect(found[9]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[11]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}', entries: [{ kind: 'pair', key: '"x"', value: '1' }] }] })
{ kind: 'map', entries: [{ kind: 'pair', key: '"a"', value: '{}' }] },
{ kind: 'map', entries: [] },
])
})
it('列表、元组、集合:各项照源码顺序,*x 是展开,注释不算一项;值里的字面量另有自己的各项,空列表没有项', async () => {
const source = [
'filters = [',
' Filter(field="user_id"),',
' # 注释不算一项',
' *rest,',
' (a, b),',
' {1},',
' [],',
']',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'sequence', elements: [
{ kind: 'item', value: 'Filter(field="user_id")' },
{ kind: 'spread', value: 'rest' },
{ kind: 'item', value: '(a, b)' },
{ kind: 'item', value: '{1}' },
{ kind: 'item', value: '[]' },
] },
{ kind: 'sequence', elements: [{ kind: 'item', value: 'a' }, { kind: 'item', value: 'b' }] },
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }] },
{ kind: 'sequence', elements: [] },
])
})
it('推导式、括号里的一个式子、调用的实参列表、别的值不是', async () => {
expect(await collections('x = [a for a in b]\ny = (a)\nf(a, b)\nz = a, b')).toEqual([])
})
})
+29 -15
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, ElementFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { pythonNameReader } from './pythonNames'
@@ -303,7 +303,7 @@ function expressionBlock(node: Node, scope: Scope, doc: Node | undefined): CodeB
facts: {
access: scopeAccess(left, scope),
doc,
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined, entries: dictionaryEntries(value) }],
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined }],
},
}
}
@@ -314,21 +314,34 @@ function assignmentIn(statement: Node): Node | undefined {
return expression?.type === 'assignment' ? expression : undefined
}
/** 字典字面量的各项:`键: 值`(值又是字典时连它的各项)、`**other`;值不是字典字面量时没有。 */
function dictionaryEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'dictionary')
/** 列表、元组、集合字面量(各项是 ElementFacts)的节点类型。 */
const SEQUENCES = new Set(['list', 'tuple', 'set'])
/** 字面量的各项:字典的 `键: 值`、`**other`,列表、元组、集合的各项、`*other`;别的节点没有。 */
function pythonCollection(node: Node): CollectionFacts | undefined {
if (node.type === 'dictionary')
return { kind: 'map', entries: syntaxChildren(node).flatMap(dictionaryEntry) }
if (!SEQUENCES.has(node.type))
return undefined
return syntaxChildren(value).flatMap((item): EntryFacts[] => {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
return {
kind: 'sequence',
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
if (element.type !== 'list_splat')
return [{ kind: 'item', value: element }]
const [spread] = syntaxChildren(element)
return spread ? [{ kind: 'spread', value: spread }] : []
}
const key = item.childForFieldName('key')
const itemValue = item.childForFieldName('value')
if (item.type !== 'pair' || !key || !itemValue)
return []
return [{ kind: 'pair', key, value: itemValue, entries: dictionaryEntries(itemValue) }]
})
}),
}
}
function dictionaryEntry(item: Node): EntryFacts[] {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
return spread ? [{ kind: 'spread', value: spread }] : []
}
const key = item.childForFieldName('key')
const value = item.childForFieldName('value')
return item.type === 'pair' && key && value ? [{ kind: 'pair', key, value }] : []
}
const ITERATION_RESULTS: Record<string, IterationResult> = {
@@ -671,6 +684,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
freshDoc,
iteration: pythonIteration,
call: pythonCall,
collection: pythonCollection,
displayNames: pythonNameReader,
}
}
+24 -1
View File
@@ -5,7 +5,7 @@ import { toBlockViews } from '../blockView'
import { blockMarks } from '../diff/viewDiff'
import { rustImplRule } from '../rules/rustImpl'
import { isCrateRoot } from '../rules/rustTree'
import { commentText, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { commentText, nodeFacts, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { createRustAdapter } from './rust'
@@ -188,6 +188,29 @@ describe('rust 语言卡片', () => {
})
})
describe('rust 字面量的各项', () => {
it('数组、元组的各项照源码顺序,注释不算一项;[值; 个数]、别的值不是', async () => {
const source = 'const A: [&str; 2] = [\n "a", // 第一个\n "b",\n];\nconst Z: [u8; 4] = [0; 4];\nconst T: (u8, u8) = (1, 2);\nfn f() { g(a, b); }'
expect(await nodeFacts(adapter, 'rust', source, node => adapter.collection?.(node))).toEqual([
{ kind: 'sequence', elements: [{ kind: 'item', value: '"a"' }, { kind: 'item', value: '"b"' }] },
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }, { kind: 'item', value: '2' }] },
])
})
it('常量的值是跨几行写的数组、元组时按项拆开:「数组 N 项」,每项一行「第 N 项 | 值」;写在一行里的照旧', async () => {
const source = 'pub const NAMES: [&str; 2] = [\n "a",\n "b",\n];\npub const PAIR: (u8, u8) = (\n 1,\n 2,\n);\npub const ONE: [u8; 2] = [1, 2];\n'
expect(viewLines(await views(source)).filter(line => /[值第|]/.test(line))).toEqual([
' 值: 数组 2 项',
' 第 1 项 | "a"',
' 第 2 项 | "b"',
' 值: 元组 2 项',
' 第 1 项 | 1',
' 第 2 项 | 2',
' 值: [1, 2]',
])
})
})
describe('rust 注释', () => {
it('连着写的 // 合成一块注释,空的 // 是分段,预览是去掉 // 的 Markdown;后面的 use 照常是导入', async () => {
const list = await views(readFixture('rustComments.rs'))
+11 -2
View File
@@ -1,8 +1,8 @@
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared'
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
@@ -15,9 +15,18 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
tokenRole: rustTokenRole,
lineComment: '//',
freshDoc,
collection: rustCollection,
}
}
/** 数组 `[a, b]`、元组 `(a, b)` 的各项;`[值; 个数]` 不是一项项写出来的,别的节点也没有。项前面的 `#[…]` 不算一项。 */
function rustCollection(node: Node): CollectionFacts | undefined {
const listed = node.type === 'tuple_expression' || (node.type === 'array_expression' && !node.childForFieldName('length'))
if (!listed)
return undefined
return { kind: 'sequence', elements: syntaxChildren(node).filter(child => child.type !== 'attribute_item').map(value => ({ kind: 'item', value })) }
}
/** extern 块本身不成一块源码,里面的签名跟所在这一层的别的项走同一条路。 */
function unwrapExtern(item: Node): Node[] {
if (item.type !== 'foreign_mod_item')
@@ -338,54 +338,13 @@ describe('typeScript 属性表信息(facts)', () => {
name: 'result',
type: 'EnvLockResult',
value: '{ total: files.length, ok: [], failed: [] }',
entries: [
{ kind: 'pair', key: 'total', value: 'files.length' },
{ kind: 'pair', key: 'ok', value: '[]' },
{ kind: 'pair', key: 'failed', value: '[]' },
],
}] })
})
it('对象字面量的各项:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前)、嵌套对象连它的各项;空对象是空数组,别的值没有', async () => {
const source = [
'const o = {',
' a: 1,',
' \'b-c\': x,',
' [k]: 2,',
' s,',
' ...rest,',
' // 注释不算一项',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' get size() { return 1 },',
' n: { d: 4, e: {} },',
'}',
'const empty = {}',
'const list = [1]',
].join('\n')
const at = (text: string): number => source.indexOf(text)
const found = await facts(source)
expect(found[1]).toMatchObject({ declarators: [{ entries: [
{ kind: 'pair', key: 'a', value: '1' },
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
{ kind: 'pair', key: '[k]', value: '2' },
{ kind: 'pair', key: 's', value: 's' },
{ kind: 'spread', value: 'rest' },
{
kind: 'method',
name: 'load',
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
},
{ kind: 'method', name: 'size', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }', entries: [
{ kind: 'pair', key: 'd', value: '4' },
{ kind: 'pair', key: 'e', value: '{}', entries: [] },
] },
] }] })
expect(found[14]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}', entries: [] }] })
expect(found[15]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
const found = await facts('const empty = {}\nconst list = [1]')
expect(found[1]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}' }] })
expect(found[2]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
})
it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
@@ -485,6 +444,72 @@ describe('typeScript 属性表信息(facts)', () => {
})
})
describe('typeScript 字面量的各项', () => {
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.collection?.(node))
it('对象:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前),注释不算一项;值里的对象另有自己的各项,空对象没有项', async () => {
const source = [
'const o = {',
' a: 1,',
' \'b-c\': x,',
' [k]: 2,',
' s,',
' ...rest,',
' // 注释不算一项',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' get size() { return 1 },',
' n: { d: 4, e: {} },',
'}',
].join('\n')
const at = (text: string): number => source.indexOf(text)
expect(await collections(source)).toEqual([
{ kind: 'map', entries: [
{ kind: 'pair', key: 'a', value: '1' },
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
{ kind: 'pair', key: '[k]', value: '2' },
{ kind: 'pair', key: 's', value: 's' },
{ kind: 'spread', value: 'rest' },
{
kind: 'method',
name: 'load',
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
},
{ kind: 'method', name: 'size', node: 'get size() { return 1 }', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }' },
] },
{ kind: 'map', entries: [{ kind: 'pair', key: 'd', value: '4' }, { kind: 'pair', key: 'e', value: '{}' }] },
{ kind: 'map', entries: [] },
])
})
it('数组:各项照源码顺序,...x 是展开,注释不算一项;值里的字面量另有自己的各项,空数组没有项', async () => {
const source = [
'const filters = [',
' new Filter({ field: \'user_id\' }),',
' // 注释不算一项',
' ...rest,',
' [],',
']',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'sequence', elements: [
{ kind: 'item', value: 'new Filter({ field: \'user_id\' })' },
{ kind: 'spread', value: 'rest' },
{ kind: 'item', value: '[]' },
] },
{ kind: 'map', entries: [{ kind: 'pair', key: 'field', value: '\'user_id\'' }] },
{ kind: 'sequence', elements: [] },
])
})
it('数组形式的解构、类型里的元组、别的值不是', async () => {
expect(await collections('const [a, b] = pair\nlet t: [string, number]\nf(a, b)')).toEqual([])
})
})
describe('typeScript 类', () => {
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
+38 -26
View File
@@ -1,4 +1,4 @@
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeclaratorFacts, ElementFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { attachComments, children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
@@ -153,36 +153,47 @@ function declaratorFacts(declarator: Node): DeclaratorFacts {
name: declarator.childForFieldName('name') ?? declarator,
type: annotatedType(declarator.childForFieldName('type')),
value: value ?? undefined,
entries: objectEntries(value),
}
}
/** 对象字面量的各项:`键: 值`(值又是对象时连它的各项)、简写 `{ a }`、`...other`、方法;值不是对象字面量时没有。 */
function objectEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'object')
/** 字面量的各项:对象的 `键: 值`、简写 `{ a }`、`...other`、方法,数组的各项、`...other`;别的节点没有。 */
function typescriptCollection(node: Node): CollectionFacts | undefined {
if (node.type === 'object')
return { kind: 'map', entries: syntaxChildren(node).flatMap(objectEntry) }
if (node.type !== 'array')
return undefined
return syntaxChildren(value).flatMap((member): EntryFacts[] => {
switch (member.type) {
case 'pair': {
const key = member.childForFieldName('key')
const memberValue = member.childForFieldName('value')
return key && memberValue ? [{ kind: 'pair', key, value: memberValue, entries: objectEntries(memberValue) }] : []
}
case 'shorthand_property_identifier':
return [{ kind: 'pair', key: member, value: member }]
case 'spread_element': {
const [spread] = syntaxChildren(member)
return spread ? [{ kind: 'spread', value: spread }] : []
}
case 'method_definition': {
const name = member.childForFieldName('name')
const body = member.childForFieldName('body')
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
}
default:
return []
return {
kind: 'sequence',
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
if (element.type !== 'spread_element')
return [{ kind: 'item', value: element }]
const [spread] = syntaxChildren(element)
return spread ? [{ kind: 'spread', value: spread }] : []
}),
}
}
function objectEntry(member: Node): EntryFacts[] {
switch (member.type) {
case 'pair': {
const key = member.childForFieldName('key')
const value = member.childForFieldName('value')
return key && value ? [{ kind: 'pair', key, value }] : []
}
})
case 'shorthand_property_identifier':
return [{ kind: 'pair', key: member, value: member }]
case 'spread_element': {
const [spread] = syntaxChildren(member)
return spread ? [{ kind: 'spread', value: spread }] : []
}
case 'method_definition': {
const name = member.childForFieldName('name')
const body = member.childForFieldName('body')
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
}
default:
return []
}
}
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;TypeScript(typed)的 `import type` 只导入类型。 */
@@ -658,6 +669,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
lineComment: '//',
freshDoc,
call: typescriptCall,
collection: typescriptCollection,
displayNames: typescriptNameReader,
}
}