✨ 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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { AttributeNames, BlockAttributes, CodeBlock, CommonAttribute } from './contract'
|
||||
import { COMMON_ATTRIBUTES } from './contract'
|
||||
|
||||
/** 这份文件里出现过的一个属性值:显示名和有几块是这个值。 */
|
||||
export interface AttributeValueCount {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
/** 这份文件里出现过的一个属性:显示名,出现过的值按第一次出现的先后排。 */
|
||||
export interface FileAttribute {
|
||||
id: string
|
||||
name: string
|
||||
values: AttributeValueCount[]
|
||||
}
|
||||
|
||||
function flag(value: boolean | undefined): string | undefined {
|
||||
return value === undefined ? undefined : String(value)
|
||||
}
|
||||
|
||||
/** 从 facts 推出的通用属性(kind 以外);facts 里没填的是 undefined。 */
|
||||
function factAttributes(block: CodeBlock): Partial<Record<CommonAttribute, string>> {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
return { access: block.facts.access, async: flag(block.facts.async) }
|
||||
case 'class':
|
||||
return { access: block.facts.access, abstract: flag(block.facts.abstract) }
|
||||
case 'import':
|
||||
return { typeOnly: flag(block.facts.typeOnly) }
|
||||
case 'const':
|
||||
case 'variable':
|
||||
return { access: block.facts.access }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/** 块的筛选属性:从 facts 推出的通用属性,叠上语言插件自己报的(同一属性以它为准),kind 总是块的种类。 */
|
||||
export function blockAttributes(block: CodeBlock): BlockAttributes {
|
||||
const derived = Object.entries(factAttributes(block)).filter((entry): entry is [string, string] => entry[1] !== undefined)
|
||||
return { ...Object.fromEntries(derived), ...block.attributes, kind: block.kind }
|
||||
}
|
||||
|
||||
interface Attributed {
|
||||
attributes: BlockAttributes
|
||||
children: Attributed[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 整份文件(所有层的块)里出现过的属性,每个值有几块。通用属性按 COMMON_ATTRIBUTES 的顺序排在前面,
|
||||
* 语言自己的属性按第一次出现的先后排在后面。显示名取翻译插件的 names,没给的用 id。
|
||||
*/
|
||||
export function fileAttributes(blocks: readonly Attributed[], names: AttributeNames | undefined): FileAttribute[] {
|
||||
const counts = new Map<string, Map<string, number>>()
|
||||
const visit = (list: readonly Attributed[]): void => {
|
||||
for (const block of list) {
|
||||
for (const [id, value] of Object.entries(block.attributes)) {
|
||||
const values = counts.get(id) ?? new Map<string, number>()
|
||||
counts.set(id, values.set(value, (values.get(value) ?? 0) + 1))
|
||||
}
|
||||
visit(block.children)
|
||||
}
|
||||
}
|
||||
visit(blocks)
|
||||
const common: readonly string[] = COMMON_ATTRIBUTES
|
||||
const rank = (id: string): number => common.includes(id) ? common.indexOf(id) : common.length
|
||||
// sort 是稳定的:语言自己的属性同排在最后,保持第一次出现的先后
|
||||
return [...counts].sort(([a], [b]) => rank(a) - rank(b)).map(([id, values]) => ({
|
||||
id,
|
||||
name: names?.attributes[id] ?? id,
|
||||
values: [...values].map(([value, count]) => ({ id: value, name: names?.values[id]?.[value] ?? value, count })),
|
||||
}))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { BlockAttributes, BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
|
||||
interface BlockViewBase {
|
||||
@@ -15,6 +16,8 @@ interface BlockViewBase {
|
||||
codeTokens: Segment[]
|
||||
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
|
||||
defines?: SourceRange
|
||||
/** 筛选属性(含 kind),见 blockAttributes。 */
|
||||
attributes: BlockAttributes
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
|
||||
code: rows.map(row => row.slice(Math.min(indent, row.length))).join('\n'),
|
||||
codeTokens: colorLines(block.node.tree.rootNode, source, lines, startRow, endRow, indent, adapter.tokenRole),
|
||||
...(name && { defines: { startIndex: name.startIndex, endIndex: name.endIndex } }),
|
||||
attributes: blockAttributes(block),
|
||||
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
|
||||
...('facts' in block
|
||||
? { properties: translator ? translator.describe(block, source) : [] }
|
||||
@@ -135,7 +139,7 @@ export async function readBlockViews(
|
||||
): Promise<BlockView[]> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
return toBlockViews(adapter.toBlocks(tree.rootNode, source), source, adapter, translator)
|
||||
return toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), source, adapter, translator)
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface FunctionFacts {
|
||||
doc?: Node
|
||||
/** 嵌套在函数体、条件等语句块里的函数没有。 */
|
||||
access?: Access
|
||||
async: boolean
|
||||
/** 异步函数;没有这个概念的语言不填。 */
|
||||
async?: boolean
|
||||
decorators: Node[]
|
||||
parameters: ParameterFacts[]
|
||||
returnType?: Node
|
||||
@@ -107,6 +108,21 @@ export interface VariableFacts {
|
||||
declarators: DeclaratorFacts[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选属性的通用词表,筛选栏按这个顺序排,语言自己的属性排在后面:
|
||||
* - kind:块的种类(BlockKind);
|
||||
* - access:访问级别,'public' | 'private' | 'protected',语言可以加自己的值(Kotlin 的 'internal',见 BlockShape.attributes);
|
||||
* - async、abstract、typeOnly:异步、抽象、仅类型,值是 'true' | 'false'。
|
||||
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、ImportFacts.typeOnly、VariableFacts.access),
|
||||
* 语言插件不用另报;facts 里没填的(语言没有这个概念、块没有这一项),块就没有这个属性,也不受这个属性的筛选影响。
|
||||
*/
|
||||
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
|
||||
|
||||
export type CommonAttribute = (typeof COMMON_ATTRIBUTES)[number]
|
||||
|
||||
/** 块的筛选属性:属性 id → 值 id,都是稳定的英文 id,不放显示文字(显示名由翻译插件给,见 AttributeNames)。 */
|
||||
export type BlockAttributes = Record<string, string>
|
||||
|
||||
export interface BlockShape {
|
||||
/** 这一块对应的语法树节点(整条语句)。 */
|
||||
node: Node
|
||||
@@ -116,6 +132,11 @@ export interface BlockShape {
|
||||
*/
|
||||
header: SourceSpan
|
||||
children: CodeBlock[]
|
||||
/**
|
||||
* 语言自己报的筛选属性,和宿主推出的通用属性(见 COMMON_ATTRIBUTES)合在一起:通用词表以外的属性(Rust 的 unsafe: 'true'),
|
||||
* 或 facts 表达不了的通用属性值(Kotlin 的 access: 'internal',盖过从 facts 推出的)。kind 总是块的种类,这里填了不算。
|
||||
*/
|
||||
attributes?: BlockAttributes
|
||||
}
|
||||
|
||||
/** 预览成属性表的块:语言插件从语法树读出结构化信息(facts),翻译插件把它排成属性表。 */
|
||||
@@ -185,11 +206,20 @@ export interface LanguageAdapter {
|
||||
languageIds: string[]
|
||||
/** 该 languageId 用的语法包 .wasm 的绝对路径(宿主负责加载,保证和解析器运行时是同一份)。 */
|
||||
grammarPath: (languageId: string) => string
|
||||
toBlocks: (root: Node, source: string) => CodeBlock[]
|
||||
/** languageId 是这份源码的 languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
|
||||
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
|
||||
/** 源码着色:给语法树叶子节点定词性。 */
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
}
|
||||
|
||||
/** 筛选属性和值的显示名,筛选栏、种类标签用它;没给名字的显示 id。 */
|
||||
export interface AttributeNames {
|
||||
/** 属性 id → 属性名,例如 access → 访问级别。 */
|
||||
attributes: Record<string, string>
|
||||
/** 属性 id → 值 id → 值名,例如 access → public → 公共、async → true → 是、kind → function → 函数。 */
|
||||
values: Record<string, Record<string, string>>
|
||||
}
|
||||
|
||||
/** 翻译插件:代码块 → 一行译文或属性表。 */
|
||||
export interface Translator {
|
||||
id: string // 'typescript.zh-template'
|
||||
@@ -199,6 +229,8 @@ export interface Translator {
|
||||
translate: (block: TextBlock, source: string) => PreviewText
|
||||
/** 函数、类、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||
/** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
|
||||
attributeNames?: AttributeNames
|
||||
}
|
||||
|
||||
export interface CodeReaderApi {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Memento } from 'vscode'
|
||||
import type { Excluded } from './blockFilter'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BlockFilter, FILTER_KEY, normalizeExcluded } from './blockFilter'
|
||||
|
||||
function memento(initial: Record<string, unknown> = {}): Pick<Memento, 'get' | 'update'> & { store: Record<string, unknown> } {
|
||||
const store = { ...initial }
|
||||
return {
|
||||
store,
|
||||
get: <T>(key: string): T | undefined => store[key] as T | undefined,
|
||||
update: async (key: string, value: unknown) => {
|
||||
store[key] = value
|
||||
},
|
||||
} as Pick<Memento, 'get' | 'update'> & { store: Record<string, unknown> }
|
||||
}
|
||||
|
||||
describe('块筛选:整个工作区共用一份', () => {
|
||||
it('记的是去掉的值,写进 workspaceState,重新打开窗口(新的实例)读得回来', async () => {
|
||||
const state = memento()
|
||||
await new BlockFilter(state).set({ access: ['private'], kind: ['comment', 'import'] })
|
||||
expect(state.store[FILTER_KEY]).toEqual({ access: ['private'], kind: ['comment', 'import'] })
|
||||
expect(new BlockFilter(state).excluded).toEqual({ access: ['private'], kind: ['comment', 'import'] })
|
||||
})
|
||||
|
||||
it('一个预览改了,所有订阅的预览都收到新的筛选;取消订阅后不再收到', async () => {
|
||||
const filter = new BlockFilter(memento())
|
||||
const first: Excluded[] = []
|
||||
const second: Excluded[] = []
|
||||
filter.subscribe(excluded => first.push(excluded))
|
||||
const off = filter.subscribe(excluded => second.push(excluded))
|
||||
await filter.set({ async: ['true'] })
|
||||
off.dispose()
|
||||
await filter.set({})
|
||||
expect(first).toEqual([{ async: ['true'] }, {}])
|
||||
expect(second).toEqual([{ async: ['true'] }])
|
||||
})
|
||||
|
||||
it('读回来的、webview 发来的逐项核对:形状不对的当没有,值去重排序,空的属性去掉', () => {
|
||||
expect(normalizeExcluded(undefined)).toEqual({})
|
||||
expect(normalizeExcluded(['access'])).toEqual({})
|
||||
expect(normalizeExcluded({ kind: ['if', 'comment', 'if', 3], access: 'private', async: [] })).toEqual({ kind: ['comment', 'if'] })
|
||||
expect(new BlockFilter(memento({ [FILTER_KEY]: 'broken' })).excluded).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Disposable, Memento } from 'vscode'
|
||||
|
||||
/** workspaceState 里记的块筛选:Excluded。 */
|
||||
export const FILTER_KEY = 'superpowers.codeReader.filter'
|
||||
|
||||
/**
|
||||
* 块筛选:属性 id → 不勾(藏起来)的值 id。记的是去掉的值而不是勾着的值:
|
||||
* 一份文件里第一次出现的值默认显示,换到没出现过这些值的文件也不用改。
|
||||
*/
|
||||
export type Excluded = Record<string, string[]>
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/** 读回来的、webview 发来的筛选逐项核对:只留字符串值,去重排序;一个值都不剩的属性去掉。 */
|
||||
export function normalizeExcluded(value: unknown): Excluded {
|
||||
if (!isRecord(value))
|
||||
return {}
|
||||
const entries = Object.entries(value).flatMap(([attribute, values]): [string, string[]][] => {
|
||||
const kept = Array.isArray(values) ? [...new Set(values.filter((item): item is string => typeof item === 'string'))].sort() : []
|
||||
return kept.length > 0 ? [[attribute, kept]] : []
|
||||
})
|
||||
return Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)))
|
||||
}
|
||||
|
||||
/**
|
||||
* 整个工作区共用的一份块筛选:记在 workspaceState 里,重新加载窗口后还在;
|
||||
* 一个预览改了,所有订阅的预览都收到(包括改的那个,webview 只按扩展端发来的显示)。
|
||||
* 不依赖 vscode 运行时,便于单元测试。
|
||||
*/
|
||||
export class BlockFilter {
|
||||
private readonly listeners = new Set<(excluded: Excluded) => void>()
|
||||
|
||||
constructor(private readonly state: Pick<Memento, 'get' | 'update'>) {}
|
||||
|
||||
get excluded(): Excluded {
|
||||
return normalizeExcluded(this.state.get(FILTER_KEY))
|
||||
}
|
||||
|
||||
/** 筛选变了就调 listener,带上新的筛选。 */
|
||||
subscribe(listener: (excluded: Excluded) => void): Disposable {
|
||||
this.listeners.add(listener)
|
||||
return { dispose: () => this.listeners.delete(listener) }
|
||||
}
|
||||
|
||||
async set(value: unknown): Promise<void> {
|
||||
const excluded = normalizeExcluded(value)
|
||||
await this.state.update(FILTER_KEY, excluded)
|
||||
for (const listener of [...this.listeners])
|
||||
listener(excluded)
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,15 @@ describe('预览的 document 消息', () => {
|
||||
expect(message.error).toBeUndefined()
|
||||
expect(message.blocks.map(b => b.kind)).toEqual(['if'])
|
||||
expect(message.blocks[0].children).toMatchObject([{ kind: 'return', translation: [{ text: '返回' }, { text: ' ' }, { text: '空数组' }] }])
|
||||
expect(message.attributes).toEqual([
|
||||
{ id: 'kind', name: '种类', values: [{ id: 'if', name: '条件', count: 1 }, { id: 'return', name: '返回', count: 1 }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('没有语言插件的语言:带 error,不解析', async () => {
|
||||
const message = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime)
|
||||
expect(message.blocks).toEqual([])
|
||||
expect(message.attributes).toEqual([])
|
||||
expect(message.translators).toEqual([])
|
||||
expect(message.error).toContain('rust')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentMessage } from './messages'
|
||||
import { fileAttributes } from '../attributes'
|
||||
import { readBlockViews } from '../blockView'
|
||||
|
||||
/** 生成预览数据要读的那点文档信息(TextDocument 的子集)。 */
|
||||
@@ -24,6 +25,7 @@ export async function documentMessage(
|
||||
version,
|
||||
languageId,
|
||||
blocks: [],
|
||||
attributes: [],
|
||||
translators: registry.translatorsFor(languageId).map(({ id, label }) => ({ id, label })),
|
||||
translatorId: translator?.id ?? '',
|
||||
}
|
||||
@@ -31,7 +33,8 @@ export async function documentMessage(
|
||||
if (!adapter)
|
||||
return { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }
|
||||
try {
|
||||
return { ...message, blocks: await readBlockViews(runtime, adapter, translator, languageId, source) }
|
||||
const blocks = await readBlockViews(runtime, adapter, translator, languageId, source)
|
||||
return { ...message, blocks, attributes: fileAttributes(blocks, translator?.attributeNames) }
|
||||
}
|
||||
catch (err) {
|
||||
return { ...message, error: err instanceof Error ? err.message : String(err) }
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
* 代码阅读预览与扩展之间的消息,与 webview 端 `webview-ui/src/codeReader/types.ts` 同构,两边手动保持一致。
|
||||
*/
|
||||
|
||||
import type { FileAttribute } from '../attributes'
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { Excluded } from './blockFilter'
|
||||
|
||||
export type { Excluded }
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
@@ -21,6 +25,8 @@ export interface DocumentMessage {
|
||||
version: number
|
||||
languageId: string
|
||||
blocks: BlockView[]
|
||||
/** 这份文件里出现过的筛选属性和值(带显示名、块数),按筛选栏的先后排,见 fileAttributes */
|
||||
attributes: FileAttribute[]
|
||||
translators: TranslatorOption[]
|
||||
translatorId: string
|
||||
error?: string
|
||||
@@ -31,11 +37,13 @@ export interface DocumentMessage {
|
||||
/**
|
||||
* reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal)
|
||||
* definitionProbed:id 那次 probeDefinition 的回答,found 表示 VS Code 查得到定义
|
||||
* filter:整个工作区共用的块筛选;ready 后先于第一份文档发一次,之后任何一个预览改了都发给所有预览
|
||||
*/
|
||||
export type ExtensionToWebview
|
||||
= | DocumentMessage
|
||||
| ({ type: 'reveal' } & Reveal)
|
||||
| { type: 'definitionProbed', id: number, found: boolean }
|
||||
| { type: 'filter', excluded: Excluded }
|
||||
|
||||
/**
|
||||
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来的缩进)
|
||||
@@ -43,9 +51,11 @@ export type ExtensionToWebview
|
||||
* probeDefinition:按住 ⌘ / Ctrl 指到带下标的名字上,问源码 offset 处有没有定义;id 标出是哪一次问,回答带回去;
|
||||
* version 是这份下标所属的文档版本
|
||||
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
|
||||
* setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览
|
||||
*/
|
||||
export type WebviewToExtension
|
||||
= | { type: 'ready' }
|
||||
| { type: 'setFilter', excluded: Excluded }
|
||||
| { type: 'edit', startLine: number, endLine: number, text: string }
|
||||
| { type: 'selectTranslator', id: string }
|
||||
| { type: 'visibleLine', line: number }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { Definition } from './definitions'
|
||||
import type { ExtensionToWebview, Reveal, WebviewToExtension } from './messages'
|
||||
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
@@ -29,13 +30,14 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
private readonly extensionUri: Uri,
|
||||
private readonly registry: CodeReaderRegistry,
|
||||
private readonly runtime: TreeSitterRuntime,
|
||||
private readonly filter: BlockFilter,
|
||||
) {}
|
||||
|
||||
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
|
||||
panel.webview.options = { enableScripts: true, localResourceRoots: [webviewUiRoot(this.extensionUri)] }
|
||||
panel.webview.html = webviewHtml(panel.webview, this.extensionUri, 'code-reader.html')
|
||||
const key = document.uri.toString()
|
||||
const preview = new Preview(document, panel, this, this.registry, this.runtime, this.reveals.get(key))
|
||||
const preview = new Preview(document, panel, this, this.registry, this.runtime, this.filter, this.reveals.get(key))
|
||||
this.reveals.delete(key)
|
||||
this.previews.add(preview)
|
||||
panel.onDidDispose(() => {
|
||||
@@ -96,6 +98,7 @@ class Preview {
|
||||
private readonly owner: PreviewEditorProvider,
|
||||
private readonly registry: CodeReaderRegistry,
|
||||
private readonly runtime: TreeSitterRuntime,
|
||||
private readonly filter: BlockFilter,
|
||||
/**
|
||||
* 随下一份真正发出去的文档走、只走一次:新预览第一份文档要滚到的位置,或页面正在重新加载时收不到的 reveal。
|
||||
* 之后页面因标签页切走重新加载,按 webview 自己记的位置恢复
|
||||
@@ -112,6 +115,11 @@ class Preview {
|
||||
if (e.document === document && e.contentChanges.length > 0)
|
||||
this.schedulePush()
|
||||
}),
|
||||
// 页面没 ready 时收不到,等它 ready 再随第一份文档之前补发
|
||||
filter.subscribe((excluded) => {
|
||||
if (this.ready)
|
||||
this.post({ type: 'filter', excluded })
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -167,8 +175,13 @@ class Preview {
|
||||
switch (msg.type) {
|
||||
case 'ready':
|
||||
this.ready = true
|
||||
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
|
||||
this.post({ type: 'filter', excluded: this.filter.excluded })
|
||||
await this.push()
|
||||
return
|
||||
case 'setFilter':
|
||||
await this.filter.set(msg.excluded)
|
||||
return
|
||||
case 'edit':
|
||||
await this.applyEdit(msg.startLine, msg.endLine, msg.text)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ExtensionContext, Uri } from 'vscode'
|
||||
import type { CodeReaderApi } from './contract'
|
||||
import { commands, window, workspace } from 'vscode'
|
||||
import { BlockFilter } from './editor/blockFilter'
|
||||
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './editor/previewEditor'
|
||||
import { showPreview, showSource } from './editor/switchEditor'
|
||||
import { FileTreeProvider } from './explorer/fileTree'
|
||||
@@ -45,7 +46,7 @@ async function openFile(uri: Uri, registry: CodeReaderRegistry): Promise<void> {
|
||||
export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
const registry = new CodeReaderRegistry(context.globalState)
|
||||
const runtime = new TreeSitterRuntime(bundledWasmDir())
|
||||
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime)
|
||||
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState))
|
||||
const notes = new FileNotes()
|
||||
const files = new FileTreeProvider(OPEN_COMMAND, notes)
|
||||
|
||||
|
||||
@@ -532,4 +532,15 @@ describe('typeScript 类', () => {
|
||||
expect(js[1]).toMatchObject({ name: 'A', bases: [{ value: 'mixin(B)' }], implements: [] })
|
||||
expect(js[2]).toMatchObject({ access: 'public', bases: [{ value: 'C' }] })
|
||||
})
|
||||
|
||||
it('javaScript / JSX 没有抽象类、仅类型导入:facts 里不填 abstract、typeOnly(不是填 false)', async () => {
|
||||
for (const languageId of ['javascript', 'javascriptreact']) {
|
||||
const js = await facts('import { a } from \'a\'\nclass K {}', languageId)
|
||||
expect(js[1]).toEqual({ sources: ['\'a\''], items: [{ kind: 'named', name: 'a' }] })
|
||||
expect(js[2]).toEqual({ name: 'K', access: 'private', decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
}
|
||||
const tsx = await facts('import { a } from \'a\'\nclass K {}', 'typescriptreact')
|
||||
expect(tsx[1]).toMatchObject({ typeOnly: false })
|
||||
expect(tsx[2]).toMatchObject({ abstract: false })
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,9 @@ const GRAMMARS: Record<string, string> = {
|
||||
javascriptreact: 'tree-sitter-javascript.wasm',
|
||||
}
|
||||
|
||||
/** 分得出抽象类、仅类型导入的 languageId;JavaScript / JSX 没有这两个概念。 */
|
||||
const TYPED_LANGUAGES = new Set(['typescript', 'typescriptreact'])
|
||||
|
||||
const FUNCTION_VALUES = new Set(['arrow_function', 'function_expression', 'function', 'generator_function'])
|
||||
const LOOPS = new Set(['for_statement', 'for_in_statement', 'while_statement', 'do_statement'])
|
||||
const CLASSES = new Set(['class_declaration', 'abstract_class_declaration', 'class'])
|
||||
@@ -41,118 +44,6 @@ function callbackBody(expression: Node | null | undefined): Node | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[] {
|
||||
return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS)
|
||||
}
|
||||
|
||||
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
|
||||
function bodyShape(outer: Node, body: Node | null): BlockShape {
|
||||
if (!body)
|
||||
return leafShape(outer)
|
||||
const items = body.type === 'statement_block' ? named(body) : [body]
|
||||
return compoundShape(outer, body.startIndex, items, (list, afterRow) => statements(list, false, afterRow))
|
||||
}
|
||||
|
||||
function bodyBlock(kind: TextKind, outer: Node, body: Node | null): CodeBlock {
|
||||
return { kind, ...bodyShape(outer, body) }
|
||||
}
|
||||
|
||||
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
switch (node.type) {
|
||||
case 'import_statement':
|
||||
return [{ kind: 'import', ...leafShape(node), facts: importFacts(node) }]
|
||||
case 'export_statement':
|
||||
return exportBlocks(node, top)
|
||||
case 'if_statement':
|
||||
return ifBlocks(node, node, 'if')
|
||||
case 'try_statement':
|
||||
return tryBlocks(node)
|
||||
case 'switch_statement':
|
||||
return [switchBlock(node)]
|
||||
case 'switch_case':
|
||||
case 'switch_default':
|
||||
return [caseBlock(node)]
|
||||
case 'return_statement':
|
||||
return [leafBlock('return', node)]
|
||||
case 'expression_statement':
|
||||
return [bodyBlock('statement', node, callbackBody(node.firstNamedChild))]
|
||||
case 'labeled_statement': {
|
||||
const [first, ...rest] = statementBlocks(node.childForFieldName('body') ?? node, top)
|
||||
return first ? [{ ...extendHeader(first, node), node }, ...rest] : []
|
||||
}
|
||||
case 'statement_block':
|
||||
return statements(named(node), top, node.startPosition.row)
|
||||
case 'empty_statement':
|
||||
return []
|
||||
default:
|
||||
if (LOOPS.has(node.type))
|
||||
return [bodyBlock('loop', node, node.childForFieldName('body'))]
|
||||
return declarationBlocks(node, node, top)
|
||||
}
|
||||
}
|
||||
|
||||
/** export 包着声明时按声明定种类,块节点取整个 export_statement(头部包含 export)。 */
|
||||
function exportBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
const declaration = node.childForFieldName('declaration')
|
||||
if (declaration)
|
||||
return declarationBlocks(declaration, node, top)
|
||||
const value = node.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
return [leafBlock('statement', node)]
|
||||
}
|
||||
|
||||
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
|
||||
switch (declaration.type) {
|
||||
case 'function_declaration':
|
||||
case 'generator_function_declaration':
|
||||
case 'function_signature':
|
||||
return [functionBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
|
||||
case 'lexical_declaration':
|
||||
case 'variable_declaration':
|
||||
return [variableBlock(declaration, outer, top)]
|
||||
case 'interface_declaration':
|
||||
case 'enum_declaration':
|
||||
return [membersBlock(declaration, outer)]
|
||||
case 'type_alias_declaration':
|
||||
return [leafBlock('type', outer)]
|
||||
default: {
|
||||
if (CLASSES.has(declaration.type))
|
||||
return [classBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
|
||||
const body = declaration.childForFieldName('body')
|
||||
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 const → const,其余 → variable。 */
|
||||
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
|
||||
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
|
||||
const access = declarationAccess(outer, top)
|
||||
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
|
||||
if (value && CLASSES.has(value.type))
|
||||
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
if (value && functionBody(value))
|
||||
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
|
||||
return {
|
||||
kind: top && isConst ? 'const' : 'variable',
|
||||
...bodyShape(outer, callbackBody(value)),
|
||||
facts: { access, declarators: declarators.map(declaratorFacts) },
|
||||
}
|
||||
}
|
||||
|
||||
/** 函数块:fn 是声明、方法或函数值,带语句块主体时子块来自主体;name 是它在源码里的名字(变量名、方法名)。 */
|
||||
function functionBlock(outer: Node, fn: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
|
||||
return {
|
||||
kind: 'function',
|
||||
...bodyShape(outer, fn.childForFieldName('body')),
|
||||
facts: functionFacts(fn, name, access, decorators),
|
||||
}
|
||||
}
|
||||
|
||||
/** 模块里的声明:export 了是公共,顶层没 export 是私有,嵌套在语句块里的没有访问级别。 */
|
||||
function declarationAccess(outer: Node, top: boolean): Access | undefined {
|
||||
if (outer.type === 'export_statement')
|
||||
@@ -242,8 +133,8 @@ function objectEntries(value: Node | null): EntryFacts[] | undefined {
|
||||
})
|
||||
}
|
||||
|
||||
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;`import type` 只导入类型。 */
|
||||
function importFacts(node: Node): ImportFacts {
|
||||
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;TypeScript(typed)的 `import type` 只导入类型。 */
|
||||
function importFacts(node: Node, typed: boolean): ImportFacts {
|
||||
const require = named(node).find(child => child.type === 'import_require_clause')
|
||||
const source = (require ?? node).childForFieldName('source')
|
||||
const items: ImportItem[] = []
|
||||
@@ -265,87 +156,22 @@ function importFacts(node: Node): ImportFacts {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { sources: source ? [source] : [], typeOnly: children(node).some(child => child.type === 'type'), items }
|
||||
}
|
||||
|
||||
/** if 的 else / else if 展平成紧跟其后的兄弟块。 */
|
||||
function ifBlocks(ifNode: Node, outer: Node, kind: 'if' | 'else'): CodeBlock[] {
|
||||
const blocks = [bodyBlock(kind, outer, ifNode.childForFieldName('consequence'))]
|
||||
const alternative = ifNode.childForFieldName('alternative')
|
||||
if (alternative) {
|
||||
const inner = named(alternative).find(child => child.type !== 'comment') ?? null
|
||||
if (inner?.type === 'if_statement')
|
||||
blocks.push(...ifBlocks(inner, alternative, 'else'))
|
||||
else
|
||||
blocks.push(bodyBlock('else', alternative, inner))
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function tryBlocks(node: Node): CodeBlock[] {
|
||||
const blocks = [bodyBlock('try', node, node.childForFieldName('body'))]
|
||||
const handler = node.childForFieldName('handler')
|
||||
if (handler)
|
||||
blocks.push(bodyBlock('catch', handler, handler.childForFieldName('body')))
|
||||
const finalizer = node.childForFieldName('finalizer')
|
||||
if (finalizer)
|
||||
blocks.push(bodyBlock('finally', finalizer, finalizer.childForFieldName('body')))
|
||||
return blocks
|
||||
}
|
||||
|
||||
function switchBlock(node: Node): CodeBlock {
|
||||
const body = node.childForFieldName('body')
|
||||
if (!body)
|
||||
return leafBlock('if', node)
|
||||
return { kind: 'if', ...compoundShape(node, body.startIndex, named(body), (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/** case / default:头部到冒号为止,子块来自它下面的语句。 */
|
||||
function caseBlock(node: Node): CodeBlock {
|
||||
const [first] = node.childrenForFieldName('body')
|
||||
const bodyStart = first?.startIndex ?? node.endIndex
|
||||
const items = named(node).filter(child => child.startIndex >= bodyStart)
|
||||
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
|
||||
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
|
||||
*/
|
||||
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
|
||||
const body = declaration.childForFieldName('body')
|
||||
const facts = classFacts(outer, declaration, name, access, decorators)
|
||||
if (!body)
|
||||
return { kind: 'class', ...leafShape(outer), facts }
|
||||
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
|
||||
}
|
||||
|
||||
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
|
||||
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
|
||||
let decorators: Node[] = []
|
||||
return listBlocks(list, afterRow, (member) => {
|
||||
if (member.type === 'decorator') {
|
||||
decorators.push(member)
|
||||
return []
|
||||
}
|
||||
const blocks = memberBlocks(member, decorators)
|
||||
const [from] = decorators
|
||||
decorators = []
|
||||
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
|
||||
}, COMMENTS)
|
||||
const sources = source ? [source] : []
|
||||
return typed ? { sources, typeOnly: children(node).some(child => child.type === 'type'), items } : { sources, items }
|
||||
}
|
||||
|
||||
/**
|
||||
* 类的装饰器写在 export 前面时挂在 export 语句上,写在 class 前面时挂在类声明上;
|
||||
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式。
|
||||
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式;
|
||||
* 只有 TypeScript(typed)分抽象类。
|
||||
*/
|
||||
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[]): ClassFacts {
|
||||
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[], typed: boolean): ClassFacts {
|
||||
const holders = sameNode(outer, declaration) ? [declaration] : [outer, declaration]
|
||||
const heritage = syntaxChildren(named(declaration).find(child => child.type === 'class_heritage'))
|
||||
return {
|
||||
name: name ?? undefined,
|
||||
access,
|
||||
abstract: declaration.type === 'abstract_class_declaration',
|
||||
...(typed && { abstract: declaration.type === 'abstract_class_declaration' }),
|
||||
decorators: [...decorators, ...holders.flatMap(holder => named(holder).filter(child => child.type === 'decorator'))],
|
||||
typeParameters: syntaxChildren(declaration.childForFieldName('type_parameters')),
|
||||
bases: heritage.flatMap((part) => {
|
||||
@@ -370,35 +196,222 @@ function extendsBases(clause: Node): BaseFacts[] {
|
||||
return bases
|
||||
}
|
||||
|
||||
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
switch (member.type) {
|
||||
case 'method_definition':
|
||||
case 'method_signature':
|
||||
case 'abstract_method_signature':
|
||||
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
case 'class_static_block':
|
||||
return [bodyBlock('statement', member, member.childForFieldName('body'))]
|
||||
default: {
|
||||
const value = member.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
return [leafBlock('field', member)]
|
||||
/**
|
||||
* 读代码块树的一套函数。typed:TypeScript(typescript、typescriptreact),分得出抽象类、仅类型导入;
|
||||
* JavaScript 没有这两个概念,facts 里不填(见 classFacts、importFacts)。
|
||||
*/
|
||||
function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
|
||||
function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[] {
|
||||
return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS)
|
||||
}
|
||||
|
||||
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
|
||||
function bodyShape(outer: Node, body: Node | null): BlockShape {
|
||||
if (!body)
|
||||
return leafShape(outer)
|
||||
const items = body.type === 'statement_block' ? named(body) : [body]
|
||||
return compoundShape(outer, body.startIndex, items, (list, afterRow) => statements(list, false, afterRow))
|
||||
}
|
||||
|
||||
function bodyBlock(kind: TextKind, outer: Node, body: Node | null): CodeBlock {
|
||||
return { kind, ...bodyShape(outer, body) }
|
||||
}
|
||||
|
||||
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
switch (node.type) {
|
||||
case 'import_statement':
|
||||
return [{ kind: 'import', ...leafShape(node), facts: importFacts(node, typed) }]
|
||||
case 'export_statement':
|
||||
return exportBlocks(node, top)
|
||||
case 'if_statement':
|
||||
return ifBlocks(node, node, 'if')
|
||||
case 'try_statement':
|
||||
return tryBlocks(node)
|
||||
case 'switch_statement':
|
||||
return [switchBlock(node)]
|
||||
case 'switch_case':
|
||||
case 'switch_default':
|
||||
return [caseBlock(node)]
|
||||
case 'return_statement':
|
||||
return [leafBlock('return', node)]
|
||||
case 'expression_statement':
|
||||
return [bodyBlock('statement', node, callbackBody(node.firstNamedChild))]
|
||||
case 'labeled_statement': {
|
||||
const [first, ...rest] = statementBlocks(node.childForFieldName('body') ?? node, top)
|
||||
return first ? [{ ...extendHeader(first, node), node }, ...rest] : []
|
||||
}
|
||||
case 'statement_block':
|
||||
return statements(named(node), top, node.startPosition.row)
|
||||
case 'empty_statement':
|
||||
return []
|
||||
default:
|
||||
if (LOOPS.has(node.type))
|
||||
return [bodyBlock('loop', node, node.childForFieldName('body'))]
|
||||
return declarationBlocks(node, node, top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** interface / enum:成员(属性、方法签名、枚举项)→ field。 */
|
||||
function membersBlock(declaration: Node, outer: Node): CodeBlock {
|
||||
const body = declaration.childForFieldName('body')
|
||||
if (!body)
|
||||
return leafBlock('type', outer)
|
||||
return {
|
||||
kind: 'type',
|
||||
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
|
||||
listBlocks(list, afterRow, member => [leafBlock('field', member)], COMMENTS)),
|
||||
/** export 包着声明时按声明定种类,块节点取整个 export_statement(头部包含 export)。 */
|
||||
function exportBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
const declaration = node.childForFieldName('declaration')
|
||||
if (declaration)
|
||||
return declarationBlocks(declaration, node, top)
|
||||
const value = node.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
return [leafBlock('statement', node)]
|
||||
}
|
||||
|
||||
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
|
||||
switch (declaration.type) {
|
||||
case 'function_declaration':
|
||||
case 'generator_function_declaration':
|
||||
case 'function_signature':
|
||||
return [functionBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
|
||||
case 'lexical_declaration':
|
||||
case 'variable_declaration':
|
||||
return [variableBlock(declaration, outer, top)]
|
||||
case 'interface_declaration':
|
||||
case 'enum_declaration':
|
||||
return [membersBlock(declaration, outer)]
|
||||
case 'type_alias_declaration':
|
||||
return [leafBlock('type', outer)]
|
||||
default: {
|
||||
if (CLASSES.has(declaration.type))
|
||||
return [classBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
|
||||
const body = declaration.childForFieldName('body')
|
||||
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 const → const,其余 → variable。 */
|
||||
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
|
||||
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
|
||||
const access = declarationAccess(outer, top)
|
||||
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
|
||||
if (value && CLASSES.has(value.type))
|
||||
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
if (value && functionBody(value))
|
||||
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
|
||||
return {
|
||||
kind: top && isConst ? 'const' : 'variable',
|
||||
...bodyShape(outer, callbackBody(value)),
|
||||
facts: { access, declarators: declarators.map(declaratorFacts) },
|
||||
}
|
||||
}
|
||||
|
||||
/** 函数块:fn 是声明、方法或函数值,带语句块主体时子块来自主体;name 是它在源码里的名字(变量名、方法名)。 */
|
||||
function functionBlock(outer: Node, fn: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
|
||||
return {
|
||||
kind: 'function',
|
||||
...bodyShape(outer, fn.childForFieldName('body')),
|
||||
facts: functionFacts(fn, name, access, decorators),
|
||||
}
|
||||
}
|
||||
|
||||
/** if 的 else / else if 展平成紧跟其后的兄弟块。 */
|
||||
function ifBlocks(ifNode: Node, outer: Node, kind: 'if' | 'else'): CodeBlock[] {
|
||||
const blocks = [bodyBlock(kind, outer, ifNode.childForFieldName('consequence'))]
|
||||
const alternative = ifNode.childForFieldName('alternative')
|
||||
if (alternative) {
|
||||
const inner = named(alternative).find(child => child.type !== 'comment') ?? null
|
||||
if (inner?.type === 'if_statement')
|
||||
blocks.push(...ifBlocks(inner, alternative, 'else'))
|
||||
else
|
||||
blocks.push(bodyBlock('else', alternative, inner))
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function tryBlocks(node: Node): CodeBlock[] {
|
||||
const blocks = [bodyBlock('try', node, node.childForFieldName('body'))]
|
||||
const handler = node.childForFieldName('handler')
|
||||
if (handler)
|
||||
blocks.push(bodyBlock('catch', handler, handler.childForFieldName('body')))
|
||||
const finalizer = node.childForFieldName('finalizer')
|
||||
if (finalizer)
|
||||
blocks.push(bodyBlock('finally', finalizer, finalizer.childForFieldName('body')))
|
||||
return blocks
|
||||
}
|
||||
|
||||
function switchBlock(node: Node): CodeBlock {
|
||||
const body = node.childForFieldName('body')
|
||||
if (!body)
|
||||
return leafBlock('if', node)
|
||||
return { kind: 'if', ...compoundShape(node, body.startIndex, named(body), (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/** case / default:头部到冒号为止,子块来自它下面的语句。 */
|
||||
function caseBlock(node: Node): CodeBlock {
|
||||
const [first] = node.childrenForFieldName('body')
|
||||
const bodyStart = first?.startIndex ?? node.endIndex
|
||||
const items = named(node).filter(child => child.startIndex >= bodyStart)
|
||||
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
|
||||
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
|
||||
*/
|
||||
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
|
||||
const body = declaration.childForFieldName('body')
|
||||
const facts = classFacts(outer, declaration, name, access, decorators, typed)
|
||||
if (!body)
|
||||
return { kind: 'class', ...leafShape(outer), facts }
|
||||
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
|
||||
}
|
||||
|
||||
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
|
||||
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
|
||||
let decorators: Node[] = []
|
||||
return listBlocks(list, afterRow, (member) => {
|
||||
if (member.type === 'decorator') {
|
||||
decorators.push(member)
|
||||
return []
|
||||
}
|
||||
const blocks = memberBlocks(member, decorators)
|
||||
const [from] = decorators
|
||||
decorators = []
|
||||
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
|
||||
}, COMMENTS)
|
||||
}
|
||||
|
||||
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
switch (member.type) {
|
||||
case 'method_definition':
|
||||
case 'method_signature':
|
||||
case 'abstract_method_signature':
|
||||
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
case 'class_static_block':
|
||||
return [bodyBlock('statement', member, member.childForFieldName('body'))]
|
||||
default: {
|
||||
const value = member.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
return [leafBlock('field', member)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** interface / enum:成员(属性、方法签名、枚举项)→ field。 */
|
||||
function membersBlock(declaration: Node, outer: Node): CodeBlock {
|
||||
const body = declaration.childForFieldName('body')
|
||||
if (!body)
|
||||
return leafBlock('type', outer)
|
||||
return {
|
||||
kind: 'type',
|
||||
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
|
||||
listBlocks(list, afterRow, member => [leafBlock('field', member)], COMMENTS)),
|
||||
}
|
||||
}
|
||||
|
||||
return root => statements(named(root), true, -1)
|
||||
}
|
||||
|
||||
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
|
||||
@@ -509,7 +522,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
|
||||
return path.join(wasmDir, file)
|
||||
},
|
||||
toBlocks: root => statements(named(root), true, -1),
|
||||
toBlocks: (root, _source, languageId) => blockReader(TYPED_LANGUAGES.has(languageId))(root),
|
||||
tokenRole: typescriptTokenRole,
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function readFixture(name: string): string {
|
||||
export async function withBlocks<T>(adapter: LanguageAdapter, languageId: string, source: string, use: (blocks: CodeBlock[]) => T): Promise<T> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
return use(adapter.toBlocks(tree.rootNode, source))
|
||||
return use(adapter.toBlocks(tree.rootNode, source, languageId))
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, ClassFacts, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, AttributeNames, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -122,6 +122,34 @@ function slotChild(node: Node, slot: Exclude<TemplatePart, string>): Node | null
|
||||
return node.namedChildren.filter(child => child && !child.isExtra)[slot.namedChild]
|
||||
}
|
||||
|
||||
const ATTRIBUTE_ZH: Record<CommonAttribute, string> = { kind: '种类', access: '访问级别', async: '异步', abstract: '抽象', typeOnly: '仅类型' }
|
||||
const KIND_ZH: Record<BlockKind, string> = {
|
||||
import: '导入',
|
||||
function: '函数',
|
||||
class: '类',
|
||||
type: '类型',
|
||||
field: '字段',
|
||||
const: '常量',
|
||||
variable: '变量',
|
||||
if: '条件',
|
||||
else: '否则',
|
||||
loop: '循环',
|
||||
try: '尝试',
|
||||
catch: '捕获',
|
||||
finally: '最终',
|
||||
return: '返回',
|
||||
statement: '语句',
|
||||
comment: '注释',
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
const ZH_ATTRIBUTE_NAMES: AttributeNames = {
|
||||
attributes: ATTRIBUTE_ZH,
|
||||
values: { kind: KIND_ZH, access: ACCESS_ZH, async: YES_NO, abstract: YES_NO, typeOnly: YES_NO },
|
||||
}
|
||||
|
||||
export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const operatorParents = new Set(table.operatorParents)
|
||||
const stringNodes = new Set(table.stringNodes)
|
||||
@@ -318,7 +346,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe }
|
||||
return { id: table.id, label: table.label, languageIds: table.languageIds, translate, describe, attributeNames: ZH_ATTRIBUTE_NAMES }
|
||||
}
|
||||
|
||||
function segment(text: string, role: TokenRole, offset: number | undefined): Segment {
|
||||
@@ -375,14 +403,13 @@ interface Phrases {
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
}
|
||||
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
|
||||
function hint(text: string): Segment {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
|
||||
function yesNo(value: boolean): Segment[] {
|
||||
return [{ text: value ? '是' : '否', role: 'storage' }]
|
||||
/** 布尔属性的一行:没有这个概念的语言(facts 里没填)没有这一行。 */
|
||||
function flagRows(attribute: 'async' | 'abstract' | 'typeOnly', value: boolean | undefined): PropertyRow[] {
|
||||
return value === undefined ? [] : [{ label: ATTRIBUTE_ZH[attribute], value: [{ text: YES_NO[`${value}`], role: 'storage' }] }]
|
||||
}
|
||||
|
||||
function count(n: number): Segment[] {
|
||||
@@ -394,7 +421,7 @@ function joined(parts: Segment[][], separator: string): Segment[] {
|
||||
}
|
||||
|
||||
function accessRows(access: Access | undefined): PropertyRow[] {
|
||||
return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
|
||||
return access ? [{ label: ATTRIBUTE_ZH.access, value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
|
||||
}
|
||||
|
||||
/** 这一行 / 子行讲的定义:几个节点(导入项是名称和别名)合起来在源码里的范围;一个节点都没有就不填。 */
|
||||
@@ -422,13 +449,13 @@ function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segmen
|
||||
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
|
||||
}
|
||||
|
||||
/** 函数:名称 / 说明 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
/** 函数:名称 / 说明 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
{ label: '异步', value: yesNo(facts.async) },
|
||||
...flagRows('async', facts.async),
|
||||
...decoratorRows(facts.decorators, say),
|
||||
{
|
||||
label: '输入参数',
|
||||
@@ -457,7 +484,7 @@ function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
...(facts.abstract === undefined ? [] : [{ label: '抽象', value: yesNo(facts.abstract) }]),
|
||||
...flagRows('abstract', facts.abstract),
|
||||
...decoratorRows(facts.decorators, say),
|
||||
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
|
||||
...listRow('父类', facts.bases.map(base => base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value))),
|
||||
@@ -493,7 +520,7 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
}
|
||||
return [
|
||||
{ label: '来源', value: facts.sources.length > 0 ? joined(facts.sources.map(say.module), ', ') : [hint('无')] },
|
||||
...(facts.typeOnly === undefined ? [] : [{ label: '仅类型', value: yesNo(facts.typeOnly) }]),
|
||||
...flagRows('typeOnly', facts.typeOnly),
|
||||
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)], ...defining(...importNodes(item)) })) },
|
||||
]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user