✨ feat(vscode): 代码阅读预览顶部加块筛选栏
语言插件按和语言无关的 id 报块属性(通用的由宿主从 facts 推),翻译插件起名,webview 只按 id 筛; 筛选整个工作区共用、记在 workspaceState;JavaScript 不再显示「抽象:否」「仅类型:否」
This commit is contained in:
1 parent
4eaf2ae11d
commit
1c7e31c51a
29 files changed
+1364
-319
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { CodeBlock, LanguageAdapter, Translator } from './contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { blockAttributes, fileAttributes } from './attributes'
|
||||
import { toBlockViews } from './blockView'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { propertyLines, WASM_DIR, withBlocks } from './testSupport'
|
||||
import { pythonZhTranslator } from './translators/pythonZh'
|
||||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||||
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
function views(adapter: LanguageAdapter, languageId: string, source: string, translator?: Translator): Promise<BlockView[]> {
|
||||
return withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, source, adapter, translator))
|
||||
}
|
||||
|
||||
/** 每块的筛选属性,按 header 起始行(1 起)索引;同一行有几块取外层那块。 */
|
||||
function attributesByLine(list: BlockView[], out: Record<number, Record<string, string>> = {}): Record<number, Record<string, string>> {
|
||||
for (const view of list) {
|
||||
out[view.startLine + 1] ??= view.attributes
|
||||
attributesByLine(view.children, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const TS_SOURCE = [
|
||||
'import type { A } from \'a\'', // 1
|
||||
'import { b } from \'b\'', // 2
|
||||
'export abstract class Repo {', // 3
|
||||
' private async load() {}', // 4
|
||||
' count = 0', // 5
|
||||
'}', // 6
|
||||
'class Plain {}', // 7
|
||||
'const LIMIT = 3', // 8
|
||||
'export function run() {', // 9
|
||||
' if (ok) {', // 10
|
||||
' return 1', // 11
|
||||
' }', // 12
|
||||
'}', // 13
|
||||
].join('\n')
|
||||
|
||||
describe('块的筛选属性:宿主从 kind 和 facts 推出通用属性', () => {
|
||||
it('typeScript:种类、访问级别、异步、抽象、仅类型;没有访问级别的块(条件、返回、字段)不带 access', async () => {
|
||||
expect(attributesByLine(await views(ts, 'typescript', TS_SOURCE))).toEqual({
|
||||
1: { kind: 'import', typeOnly: 'true' },
|
||||
2: { kind: 'import', typeOnly: 'false' },
|
||||
3: { kind: 'class', access: 'public', abstract: 'true' },
|
||||
4: { kind: 'function', access: 'private', async: 'true' },
|
||||
5: { kind: 'field' },
|
||||
7: { kind: 'class', access: 'private', abstract: 'false' },
|
||||
8: { kind: 'const', access: 'private' },
|
||||
9: { kind: 'function', access: 'public', async: 'false' },
|
||||
10: { kind: 'if' },
|
||||
11: { kind: 'return' },
|
||||
})
|
||||
})
|
||||
|
||||
it('tSX 和 TypeScript 一样分抽象类、仅类型导入', async () => {
|
||||
const found = attributesByLine(await views(ts, 'typescriptreact', 'import type { A } from \'a\'\nabstract class B {}\nconst C = () => { return <div /> }'))
|
||||
expect(found[1]).toEqual({ kind: 'import', typeOnly: 'true' })
|
||||
expect(found[2]).toEqual({ kind: 'class', access: 'private', abstract: 'true' })
|
||||
expect(found[3]).toEqual({ kind: 'function', access: 'private', async: 'false' })
|
||||
})
|
||||
|
||||
it('javaScript / JSX 没有抽象类、仅类型导入:块上没有这两个属性,属性表里也没有这两行', async () => {
|
||||
const source = 'import { b } from \'b\'\nexport class K {}\nasync function f() {}'
|
||||
for (const languageId of ['javascript', 'javascriptreact']) {
|
||||
const list = await views(ts, languageId, source, typescriptZhTranslator)
|
||||
expect(attributesByLine(list)).toEqual({
|
||||
1: { kind: 'import' },
|
||||
2: { kind: 'class', access: 'public' },
|
||||
3: { kind: 'function', access: 'private', async: 'true' },
|
||||
})
|
||||
const rows = list.map(view => 'properties' in view ? propertyLines(view.properties) : [])
|
||||
expect(rows[0]).toEqual(['来源: \'b\'', '导入项: 1 个', ' b'])
|
||||
expect(rows[1]).toEqual(['名称: K', '访问级别: 公共'])
|
||||
}
|
||||
})
|
||||
|
||||
it('python:访问级别、异步;没有抽象、仅类型', async () => {
|
||||
const source = [
|
||||
'import os', // 1
|
||||
'class _Hidden:', // 2
|
||||
' async def run(self):', // 3
|
||||
' pass', // 4
|
||||
'LIMIT = 3', // 5
|
||||
'def load():', // 6
|
||||
' for x in y:', // 7
|
||||
' pass', // 8
|
||||
].join('\n')
|
||||
const found = attributesByLine(await views(py, 'python', source))
|
||||
expect(found[1]).toEqual({ kind: 'import' })
|
||||
expect(found[2]).toEqual({ kind: 'class', access: 'private' })
|
||||
expect(found[3]).toEqual({ kind: 'function', access: 'public', async: 'true' })
|
||||
expect(found[5]).toEqual({ kind: 'const', access: 'public' })
|
||||
expect(found[6]).toEqual({ kind: 'function', access: 'public', async: 'false' })
|
||||
expect(found[7]).toEqual({ kind: 'loop' })
|
||||
})
|
||||
|
||||
it('语言插件自己报的属性叠在上面:加新属性、盖过 facts 推出的值(Kotlin 的 internal),kind 总是块的种类', async () => {
|
||||
await withBlocks(ts, 'typescript', 'export function f() {}', ([block]) => {
|
||||
const extended: CodeBlock = { ...block, attributes: { access: 'internal', unsafe: 'true', kind: 'other' } }
|
||||
expect(blockAttributes(extended)).toEqual({ kind: 'function', access: 'internal', async: 'false', unsafe: 'true' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function view(id: string, attributes: Record<string, string>, children: BlockView[] = []): BlockView {
|
||||
return { id, kind: 'statement', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes, children, translation: [] }
|
||||
}
|
||||
|
||||
describe('文件里出现过的属性', () => {
|
||||
const blocks = [
|
||||
view('0', { kind: 'const', unsafe: 'true', access: 'private' }),
|
||||
view('1', { kind: 'function', access: 'public', async: 'false', pure: 'yes' }, [
|
||||
view('1.0', { kind: 'if' }),
|
||||
view('1.1', { kind: 'function', async: 'true', unsafe: 'false' }),
|
||||
]),
|
||||
view('2', { kind: 'import', typeOnly: 'false' }),
|
||||
]
|
||||
|
||||
it('通用属性按契约的顺序排在前面,语言自己的按第一次出现的先后排在后面;值按第一次出现的先后,带所有层的块数', () => {
|
||||
const found = fileAttributes(blocks, undefined)
|
||||
expect(found.map(attribute => attribute.id)).toEqual(['kind', 'access', 'async', 'typeOnly', 'unsafe', 'pure'])
|
||||
expect(found[0].values).toEqual([
|
||||
{ id: 'const', name: 'const', count: 1 },
|
||||
{ id: 'function', name: 'function', count: 2 },
|
||||
{ id: 'if', name: 'if', count: 1 },
|
||||
{ id: 'import', name: 'import', count: 1 },
|
||||
])
|
||||
expect(found[4].values.map(value => [value.id, value.count])).toEqual([['true', 1], ['false', 1]])
|
||||
})
|
||||
|
||||
it('显示名取翻译插件给的,没给的属性、值显示 id', () => {
|
||||
const found = fileAttributes(blocks, { attributes: { access: '访问级别' }, values: { access: { public: '公共' } } })
|
||||
expect(found[1]).toEqual({ id: 'access', name: '访问级别', values: [{ id: 'private', name: 'private', count: 1 }, { id: 'public', name: '公共', count: 1 }] })
|
||||
expect(found[0].name).toBe('kind')
|
||||
})
|
||||
|
||||
it('中文直译给通用属性和值起名,和属性表里的叫法一样', async () => {
|
||||
const list = await views(ts, 'typescript', TS_SOURCE, typescriptZhTranslator)
|
||||
const found = fileAttributes(list, typescriptZhTranslator.attributeNames)
|
||||
const names = Object.fromEntries(found.map(attribute => [attribute.name, attribute.values.map(value => `${value.name} ${value.count}`)]))
|
||||
expect(names).toEqual({
|
||||
种类: ['导入 2', '类 2', '函数 2', '字段 1', '常量 1', '条件 1', '返回 1'],
|
||||
访问级别: ['公共 2', '私有 3'],
|
||||
异步: ['是 1', '否 1'],
|
||||
抽象: ['是 1', '否 1'],
|
||||
仅类型: ['是 1', '否 1'],
|
||||
})
|
||||
const labels = list.flatMap(block => 'properties' in block ? block.properties.map(row => row.label) : [])
|
||||
expect(labels).toEqual(expect.arrayContaining(['访问级别', '抽象', '仅类型']))
|
||||
expect(pythonZhTranslator.attributeNames).toEqual(typescriptZhTranslator.attributeNames)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user