🐛 fix(vscode): 代码阅读跳到参数、导入项、多变量声明里的一个时只闪属性表里那一行,按源码位置判断
This commit is contained in:
1 parent
5b4ca903c5
commit
3ac1c783fb
14 files changed
+367
-47
No files matched your search
@@ -150,6 +150,15 @@ describe('blockView', () => {
|
||||
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }])
|
||||
})
|
||||
|
||||
it('块自己的名字(defines):函数名、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块没有', async () => {
|
||||
const own = (source: string, list: BlockView[]): Record<string, string | undefined> =>
|
||||
Object.fromEntries(list.map(view => [view.id, view.defines && source.slice(view.defines.startIndex, view.defines.endIndex)]))
|
||||
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass'].join('\n')
|
||||
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined })
|
||||
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1'].join('\n')
|
||||
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h' })
|
||||
})
|
||||
|
||||
it('\\r\\n 和制表符缩进:code 用 \\n 连接,去掉共同的制表符', async () => {
|
||||
const source = 'function f() {\r\n\tif (a &&\r\n\t\tb) {\r\n\t\treturn 1\r\n\t}\r\n}\r\n'
|
||||
const list = await views(ts, 'typescript', undefined, source)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PropertyRow, Segment, TokenRole, Translator } from './contract'
|
||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
|
||||
@@ -13,6 +13,8 @@ interface BlockViewBase {
|
||||
code: string
|
||||
/** 源码着色(拼起来等于 code)。 */
|
||||
codeTokens: Segment[]
|
||||
/** 块自己的名字(见 ownName)在源码里的范围:跳到的定义就是它时闪整块,不闪属性表里的某一行。 */
|
||||
defines?: SourceRange
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
@@ -80,6 +82,22 @@ function colorLines(root: Node, source: string, lines: Line[], startRow: number,
|
||||
return segments
|
||||
}
|
||||
|
||||
/**
|
||||
* 这块本身就是哪个名字的定义:函数名,只声明一个变量(或一个解构模式)时的变量名。
|
||||
* 导入、一条语句声明几个变量时没有,它们的名字各是属性表里的一行(PropertyRow.defines)。
|
||||
*/
|
||||
function ownName(block: CodeBlock): Node | undefined {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
return block.facts.name
|
||||
case 'const':
|
||||
case 'variable':
|
||||
return block.facts.declarators.length === 1 ? block.facts.declarators[0].name : undefined
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 代码块树 → 视图数据:header 整行源码去共同缩进、按语法树着色、调用翻译插件。 */
|
||||
export function toBlockViews(blocks: CodeBlock[], source: string, adapter: LanguageAdapter, translator: Translator | undefined): BlockView[] {
|
||||
const lines = lineRanges(source)
|
||||
@@ -88,6 +106,7 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
|
||||
const { startRow, endRow } = block.header
|
||||
const rows = texts.slice(startRow, endRow + 1)
|
||||
const indent = commonIndent(rows).length
|
||||
const name = ownName(block)
|
||||
return {
|
||||
id,
|
||||
kind: block.kind,
|
||||
@@ -95,6 +114,7 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
|
||||
endLine: endRow,
|
||||
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 } }),
|
||||
children: block.children.map((child, i) => build(child, `${id}.${i}`)),
|
||||
...('facts' in block
|
||||
? { properties: translator ? translator.describe(block, source) : [] }
|
||||
|
||||
@@ -7,8 +7,11 @@ export type BlockKind
|
||||
= | 'import' | 'function' | 'class' | 'type' | 'field' | 'const' | 'variable'
|
||||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||||
|
||||
/** 源码里的一段,行号从 0 开始;下标是源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
|
||||
export interface SourceSpan { startIndex: number, endIndex: number, startRow: number, endRow: number }
|
||||
/** 源码里一段的下标:源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
|
||||
export interface SourceRange { startIndex: number, endIndex: number }
|
||||
|
||||
/** 源码里的一段,带起止行号(从 0 开始)。 */
|
||||
export interface SourceSpan extends SourceRange { startRow: number, endRow: number }
|
||||
|
||||
/** 访问级别。 */
|
||||
export type Access = 'public' | 'private' | 'protected'
|
||||
@@ -107,11 +110,17 @@ export type TokenRole
|
||||
*/
|
||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
||||
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值。 */
|
||||
export interface PropertyItem { cells: Segment[][] }
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
|
||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项)。 */
|
||||
export interface PropertyRow {
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
value: Segment[]
|
||||
items?: PropertyItem[]
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
/**
|
||||
* 让预览滚到包含 line 的块。definition 只在跳到定义时有:定义处那个名字在源码里的下标(Segment.offset),
|
||||
* 预览展开把它折叠住的块和属性行,再让那块闪几下。
|
||||
* 预览展开把它折叠住的块和属性行,再让就是这处定义的那一块或属性表里那一行闪几下。
|
||||
*/
|
||||
export interface Reveal {
|
||||
line: number
|
||||
|
||||
@@ -55,9 +55,9 @@ function texts(map: Map<number, Translated>): Record<number, string> {
|
||||
return Object.fromEntries([...map].map(([line, { segments }]) => [line, text(segments)]))
|
||||
}
|
||||
|
||||
/** 只看文字和着色:去掉各段的源码下标(下标另有用例)。 */
|
||||
/** 只看文字和着色:去掉各段的源码下标、属性行讲的定义(这两样另有用例)。 */
|
||||
function colors<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value, (key, field: unknown) => key === 'offset' ? undefined : field)) as T
|
||||
return JSON.parse(JSON.stringify(value, (key, field: unknown) => key === 'offset' || key === 'defines' ? undefined : field)) as T
|
||||
}
|
||||
|
||||
const CJK = /\p{Script=Han}/u
|
||||
@@ -578,6 +578,35 @@ describe('源码下标(跳到定义用)', () => {
|
||||
expect(linked(map.get(4)?.segments)).toEqual([['opts', at('opts ===')]])
|
||||
})
|
||||
|
||||
it('属性行、子行标出它讲的定义:参数名(连前缀、整个解构模式)、导入项的名称到别名、每个声明的变量名,其余行不标', async () => {
|
||||
/** 标了 defines 的行写成「属性名: 那段源码」,子行缩进两格只写那段源码。 */
|
||||
const marked = (source: string, rows: PropertyRow[] | undefined): string[] => (rows ?? []).flatMap(row => [
|
||||
...(row.defines ? [`${row.label}: ${source.slice(row.defines.startIndex, row.defines.endIndex)}`] : []),
|
||||
...(row.items ?? []).flatMap(item => item.defines ? [` ${source.slice(item.defines.startIndex, item.defines.endIndex)}`] : []),
|
||||
])
|
||||
const pySource = [
|
||||
'from m import a as b, c',
|
||||
'import os.path as p',
|
||||
'def f(self, *args, key: int = 1, **kw): pass',
|
||||
'x = 1',
|
||||
].join('\n')
|
||||
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
|
||||
expect(marked(pySource, pyRows.get(1))).toEqual([' a as b', ' c'])
|
||||
expect(marked(pySource, pyRows.get(2))).toEqual([' os.path as p'])
|
||||
expect(marked(pySource, pyRows.get(3))).toEqual([' self', ' *args', ' key', ' **kw'])
|
||||
expect(marked(pySource, pyRows.get(4))).toEqual(['名称: x'])
|
||||
|
||||
const tsSource = [
|
||||
'import d, * as ns from \'m\'',
|
||||
'function g({ u, v }: T, w?: number) {}',
|
||||
'let i = 0, j',
|
||||
].join('\n')
|
||||
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource)
|
||||
expect(marked(tsSource, tsRows.get(1))).toEqual([' d', ' ns'])
|
||||
expect(marked(tsSource, tsRows.get(2))).toEqual([' { u, v }', ' w'])
|
||||
expect(marked(tsSource, tsRows.get(3))).toEqual(['名称: i', '名称: j'])
|
||||
})
|
||||
|
||||
it('样例全文:带下标的只有照抄的名字和字符串,下标处的源码就是这段;变量名、函数名都带,译词、说明词都不带', async () => {
|
||||
const samples: [LanguageAdapter, string, Translator, string][] = [
|
||||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -388,6 +388,25 @@ function accessRows(access: Access | undefined): PropertyRow[] {
|
||||
return access ? [{ label: '访问级别', value: [{ text: ACCESS_ZH[access], role: 'keyword' }] }] : []
|
||||
}
|
||||
|
||||
/** 这一行 / 子行讲的定义:几个节点(导入项是名称和别名)合起来在源码里的范围;一个节点都没有就不填。 */
|
||||
function defining(...nodes: (Node | undefined)[]): Defines {
|
||||
const present = nodes.filter((node): node is Node => node !== undefined)
|
||||
if (present.length === 0)
|
||||
return {}
|
||||
return { defines: { startIndex: Math.min(...present.map(node => node.startIndex)), endIndex: Math.max(...present.map(node => node.endIndex)) } }
|
||||
}
|
||||
|
||||
function importNodes(item: ImportItem): (Node | undefined)[] {
|
||||
switch (item.kind) {
|
||||
case 'named':
|
||||
return [item.name, item.alias]
|
||||
case 'module':
|
||||
return [item.module, item.alias]
|
||||
default:
|
||||
return [item.alias]
|
||||
}
|
||||
}
|
||||
|
||||
function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segment[] {
|
||||
if (!alias)
|
||||
return item
|
||||
@@ -413,6 +432,7 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
param.type ? say.translated(param.type) : [hint('未标注')],
|
||||
param.default ? [hint('默认 '), ...say.translated(param.default)] : [],
|
||||
],
|
||||
...defining(param.name),
|
||||
})),
|
||||
},
|
||||
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
|
||||
@@ -437,7 +457,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) }]),
|
||||
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)] })) },
|
||||
{ label: '导入项', value: count(facts.items.length), items: facts.items.map(item => ({ cells: [itemText(item)], ...defining(...importNodes(item)) })) },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -445,7 +465,7 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
const valueLabel = block.kind === 'const' ? '值' : '初始值'
|
||||
return block.facts.declarators.flatMap(declarator => [
|
||||
{ label: '名称', value: say.translated(declarator.name) },
|
||||
{ label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) },
|
||||
...accessRows(block.facts.access),
|
||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||
{ label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] },
|
||||
|
||||
Reference in new issue
Block a user