✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新
This commit is contained in:
1 parent
ebf228cdc7
commit
ecc415d763
30 files changed
+1033
-157
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { LanguageAdapter, Translator } from './contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readBlockViews, toBlockViews } from './blockView'
|
||||
import { readBlockViews, readDefinitionKind, toBlockViews } from './blockView'
|
||||
import { replaceLines } from './editText'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
@@ -257,4 +257,25 @@ describe('blockView', () => {
|
||||
])
|
||||
expect(translationText(list[0].children[0])).toBe('返回 非 x')
|
||||
})
|
||||
|
||||
it('跨几行写的调用:一行译文下面带摊开实参的表;calleeKind 交给翻译插件分新建还是调用', async () => {
|
||||
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
|
||||
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source, undefined, call => call.callee.text === 'Detail' ? 'class' : undefined)
|
||||
const returned = list[0].children[0]
|
||||
expect(translationText(returned)).toBe('返回 新建 Detail · 1 个参数')
|
||||
expect('table' in returned && returned.table).toMatchObject({
|
||||
columns: ['参数', '值'],
|
||||
items: [{ cells: [[{ text: 'id', offset: source.indexOf('id=') }], [{ text: 'user' }, { text: '.' }, { text: 'id' }]] }],
|
||||
})
|
||||
})
|
||||
|
||||
it('readDefinitionKind:offset 落在哪块自己的名字上就是那块的种类,里层的块优先;不在名字上没有', async () => {
|
||||
const source = 'class UserService:\n def update_user(self):\n pass\n\nclass NotFound(Exception):\n pass\n\nlimit = 3\n'
|
||||
const kind = (text: string): Promise<string | undefined> => readDefinitionKind(runtime, py, 'python', source, source.indexOf(text))
|
||||
expect(await kind('UserService')).toBe('class')
|
||||
expect(await kind('update_user')).toBe('function')
|
||||
expect(await kind('NotFound')).toBe('exception')
|
||||
expect(await kind('limit')).toBe('variable')
|
||||
expect(await kind('pass')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockAttributes, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, Markdown, Node, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
|
||||
import type { BlockAttributes, BlockKind, CodeBlock, DisplayName, KindBadge, LanguageAdapter, LibraryRule, Markdown, Node, PreviewTable, PreviewText, PropertyRow, RuleContext, Segment, SourceDocument, SourceRange, TextBlock, TokenRole, Translator } from './contract'
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
@@ -33,9 +33,9 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
|
||||
* 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文,预览排在译文下面。
|
||||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(TextTranslation.table),块的说明(TextBlock.doc,翻译插件给的 Markdown 原文)。
|
||||
*/
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
|
||||
interface Line { start: number, end: number }
|
||||
|
||||
@@ -159,10 +159,13 @@ export function toBlockViews(blocks: CodeBlock[], document: SourceDocument, tran
|
||||
codeTokens: colorLines(root, source, lines, startRow, endRow, indent, adapter.tokenRole),
|
||||
}
|
||||
}
|
||||
/** 一行译文,带说明的块连着说明。 */
|
||||
const textView = (block: TextBlock): { translation: PreviewText, doc?: Markdown } => {
|
||||
const doc = block.doc && translator?.doc(block.doc, document)
|
||||
return { translation: translator ? translator.translate(block, document) : [], ...(doc && { doc }) }
|
||||
/** 一行译文,连着摊开实参的表、块的说明(有才有)。 */
|
||||
const textView = (block: TextBlock): { translation: PreviewText, table?: PreviewTable, doc?: Markdown } => {
|
||||
if (!translator)
|
||||
return { translation: [] }
|
||||
const { text, table } = translator.translate(block, document)
|
||||
const doc = block.doc && translator.doc(block.doc, document)
|
||||
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
|
||||
}
|
||||
/** 按源码先后逐块给位置记号:先序遍历,块的记号在它的子块之前要 */
|
||||
const anchors = new BlockAnchors()
|
||||
@@ -215,7 +218,10 @@ export interface DocumentViews {
|
||||
displayNames: DisplayName[]
|
||||
}
|
||||
|
||||
/** 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。 */
|
||||
/**
|
||||
* 解析一份源码并转成视图数据(中文名记号交给翻译插件去掉);语法树用完即释放。
|
||||
* calleeKind 交出调用的名字的定义眼下查到是哪种块(SourceDocument.calleeKind)。
|
||||
*/
|
||||
export async function readBlockViews(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
adapter: LanguageAdapter,
|
||||
@@ -223,14 +229,46 @@ export async function readBlockViews(
|
||||
languageId: string,
|
||||
source: string,
|
||||
rules: RuleRun = NO_RULES,
|
||||
calleeKind?: SourceDocument['calleeKind'],
|
||||
): Promise<DocumentViews> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
const { names, markers } = adapter.displayNames?.find(tree.rootNode, source, languageId) ?? { names: [], markers: [] }
|
||||
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), { source, language: adapter, markers }, translator, rules)
|
||||
const document: SourceDocument = { source, language: adapter, markers, ...(calleeKind && { calleeKind }) }
|
||||
const blocks = toBlockViews(adapter.toBlocks(tree.rootNode, source, languageId), document, translator, rules)
|
||||
return { blocks, displayNames: names }
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** 块树里自己的名字(见 ownName)盖住 offset 的最里层那块的种类;offset 不在哪块的名字上时没有。 */
|
||||
export function blockKindAt(blocks: readonly CodeBlock[], offset: number): BlockKind | undefined {
|
||||
for (const block of blocks) {
|
||||
const inner = blockKindAt(block.children, offset)
|
||||
if (inner)
|
||||
return inner
|
||||
const name = ownName(block)
|
||||
if (name && name.startIndex <= offset && offset < name.endIndex)
|
||||
return block.kind
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 解析一份源码,offset 处的名字定义的是哪种块(见 blockKindAt);语法树用完即释放。 */
|
||||
export async function readDefinitionKind(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
adapter: LanguageAdapter,
|
||||
languageId: string,
|
||||
source: string,
|
||||
offset: number,
|
||||
): Promise<BlockKind | undefined> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
return blockKindAt(adapter.toBlocks(tree.rootNode, source, languageId), offset)
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { BlockKind, CallFacts, Node } from './contract'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DocumentCallees } from './calleeKinds'
|
||||
|
||||
/** 一次调用:只用到被调用的表达式原文,和最后那个名字的下标。 */
|
||||
function call(callee: string, nameAt?: number): CallFacts {
|
||||
const facts = { callee: { text: callee } as Node, argumentList: {} as Node, arguments: [] }
|
||||
return nameAt === undefined ? facts : { ...facts, name: { startIndex: nameAt } as Node }
|
||||
}
|
||||
|
||||
/** 各下标处名字的定义是哪种块(语言扩展答的;没有的是没答);记下问了哪些下标。 */
|
||||
function source(kinds: Record<number, BlockKind | null>): { asked: number[], kindOf: (offset: number) => Promise<BlockKind | null | undefined> } {
|
||||
const asked: number[] = []
|
||||
return {
|
||||
asked,
|
||||
kindOf: async (offset) => {
|
||||
asked.push(offset)
|
||||
return kinds[offset]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('调用的名字定义成哪种块(DocumentCallees)', () => {
|
||||
it('第一遍当没查到,按被调用的表达式原文记下问了什么(写法一样的按一处问);查到后叫宿主重排,再排时交出来', async () => {
|
||||
let changed = 0
|
||||
const callees = new DocumentCallees(() => changed++, [])
|
||||
const first = callees.session()
|
||||
expect(first.calleeKind(call('UserDetailResponse', 40))).toBeUndefined()
|
||||
expect(first.calleeKind(call('UserDetailResponse', 90))).toBeUndefined()
|
||||
expect(first.calleeKind(call('user_service.update_user', 120))).toBeUndefined()
|
||||
expect(first.requests()).toEqual([{ callee: 'UserDetailResponse', offset: 40 }, { callee: 'user_service.update_user', offset: 120 }])
|
||||
|
||||
const from = source({ 40: 'class', 120: 'function' })
|
||||
callees.resolve(1, first.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(changed).toBe(1))
|
||||
const second = callees.session()
|
||||
expect(second.calleeKind(call('UserDetailResponse', 12))).toBe('class')
|
||||
expect(second.calleeKind(call('user_service.update_user', 7))).toBe('function')
|
||||
|
||||
// 同一版本再发一遍:问过的不再查
|
||||
callees.resolve(1, first.requests(), from.kindOf)
|
||||
await Promise.resolve()
|
||||
expect(from.asked).toEqual([40, 120])
|
||||
})
|
||||
|
||||
it('被调用的不是名字(没有 name)不问;查了没有(null)不算变了', async () => {
|
||||
let changed = 0
|
||||
const callees = new DocumentCallees(() => changed++, [])
|
||||
const session = callees.session()
|
||||
expect(session.calleeKind(call('handlers[k]'))).toBeUndefined()
|
||||
session.calleeKind(call('build', 5))
|
||||
expect(session.requests()).toEqual([{ callee: 'build', offset: 5 }])
|
||||
const from = source({ 5: null })
|
||||
callees.resolve(1, session.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(from.asked).toEqual([5]))
|
||||
await Promise.resolve()
|
||||
expect(changed).toBe(0)
|
||||
})
|
||||
|
||||
it('语言扩展还没起来(没答):隔一会儿只再查没答的,查到了再叫重排', async () => {
|
||||
let changed = 0
|
||||
const pending: (() => void)[] = []
|
||||
const scheduled: number[] = []
|
||||
const callees = new DocumentCallees(() => changed++, [5, 10], (task, ms) => {
|
||||
scheduled.push(ms)
|
||||
pending.push(task)
|
||||
return setTimeout(() => {}, 0)
|
||||
})
|
||||
const session = callees.session()
|
||||
session.calleeKind(call('Detail', 40))
|
||||
session.calleeKind(call('run', 60))
|
||||
const kinds: Record<number, BlockKind | null> = { 60: 'function' }
|
||||
const from = source(kinds)
|
||||
callees.resolve(1, session.requests(), from.kindOf)
|
||||
await vi.waitFor(() => expect(scheduled).toEqual([5]))
|
||||
expect(changed).toBe(1)
|
||||
|
||||
kinds[40] = 'class'
|
||||
pending.shift()?.()
|
||||
await vi.waitFor(() => expect(changed).toBe(2))
|
||||
expect(from.asked).toEqual([40, 60, 40])
|
||||
expect(callees.session().calleeKind(call('Detail', 40))).toBe('class')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { BlockKind, CallFacts } from './contract'
|
||||
import { DeferredAnswers } from './deferredAnswers'
|
||||
|
||||
/**
|
||||
* 调用的名字的定义是哪种块(SourceDocument.calleeKind):翻译插件靠它分新建对象还是调用函数。
|
||||
* 按被调用的表达式原文记(`user_service.update_user`),同一份文档里写法一样的调用按一处查;问语言扩展时问它最后那个名字。这里不碰 VS Code。
|
||||
*/
|
||||
|
||||
/** 问的一次调用:被调用的表达式原文,和它最后那个名字在源码里的下标。 */
|
||||
export interface CalleeRequest { callee: string, offset: number }
|
||||
|
||||
/** 这份文档里 offset 处名字的定义是哪种块:查了没有是 null,语言扩展没答(还没起来)是 undefined。 */
|
||||
export type CalleeSource = (offset: number) => Promise<BlockKind | null | undefined>
|
||||
|
||||
/** 排一遍视图数据用的:交给翻译插件的 calleeKind,和这一遍问了哪些调用。 */
|
||||
export interface CalleeSession {
|
||||
calleeKind: (call: CallFacts) => BlockKind | undefined
|
||||
requests: () => CalleeRequest[]
|
||||
}
|
||||
|
||||
/** 一份打开着的文档里调用的名字查到定义成哪种块;什么时候查、查到了怎么叫宿主重排见 DeferredAnswers。 */
|
||||
export class DocumentCallees {
|
||||
private readonly answers: DeferredAnswers<CalleeRequest, BlockKind>
|
||||
|
||||
constructor(changed: () => void, delays?: readonly number[], schedule?: (run: () => void, ms: number) => ReturnType<typeof setTimeout>) {
|
||||
this.answers = new DeferredAnswers(({ callee }) => callee, (a, b) => (a ?? null) === (b ?? null), changed, delays, schedule)
|
||||
}
|
||||
|
||||
/** 被调用的不是名字(没有 CallFacts.name)的不问,按调用算。 */
|
||||
session(): CalleeSession {
|
||||
const session = this.answers.session()
|
||||
return {
|
||||
calleeKind: call => call.name ? session.ask({ callee: call.callee.text, offset: call.name.startIndex }) : undefined,
|
||||
requests: session.questions,
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版问的调用:这个版本查过的不再查。 */
|
||||
resolve(version: number, requests: readonly CalleeRequest[], source: CalleeSource): void {
|
||||
this.answers.resolve(version, requests, ({ offset }) => source(offset))
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.answers.dispose()
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,32 @@ export interface IterationFacts {
|
||||
sharedParentheses?: { open: Node, close: Node }
|
||||
}
|
||||
|
||||
/** 调用里的一个实参,照源码顺序;注释不算。 */
|
||||
export type CallArgument
|
||||
= | { kind: 'positional', value: Node } // 按位置传的
|
||||
| { kind: 'keyword', name: Node, value: Node } // 按名字传的:Python 的 name=value
|
||||
| { kind: 'spread', value: Node } // 展开成按位置传的几个:*args、...args;value 是展开的那个表达式
|
||||
| { kind: 'keywordSpread', value: Node } // 展开成按名字传的几个:Python 的 **kwargs
|
||||
|
||||
/**
|
||||
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用排成
|
||||
* 「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
|
||||
*/
|
||||
export interface CallFacts {
|
||||
/** 被调用的表达式:`user_service.update_user`、`UserDetailResponse`;TypeScript 的 new 不算在里面。 */
|
||||
callee: Node
|
||||
/** callee 里最后那个名字(点分名字的最后一节):着色、查定义用它;callee 不是名字(`f()()`、`handlers[k]()`)时没有。 */
|
||||
name?: Node
|
||||
/**
|
||||
* 写法本身说明了是不是新建对象时给:TypeScript 的 `new X()` 是,不带 new 的调用不是。
|
||||
* Python 新建对象和调用函数写法一样,不给,翻译插件问 name 的定义是哪种块(SourceDocument.calleeKind)。
|
||||
*/
|
||||
construct?: boolean
|
||||
/** 括号连同里面的实参:它跨了几行,这次调用就是跨几行写的。 */
|
||||
argumentList: Node
|
||||
arguments: CallArgument[]
|
||||
}
|
||||
|
||||
export interface DeclaratorFacts {
|
||||
/** 变量名;解构声明是整个模式。 */
|
||||
name: Node
|
||||
@@ -331,6 +357,18 @@ export interface PropertyRow extends Defines {
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
|
||||
export interface PreviewTable {
|
||||
columns: string[]
|
||||
items: PropertyItem[]
|
||||
}
|
||||
|
||||
/** 一行译文的块翻出来的:一行译文(注释块是 Markdown 说明);跨几行写的调用还有摊开各个实参的表,预览排在这一行下面。 */
|
||||
export interface TextTranslation {
|
||||
text: PreviewText
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。
|
||||
* 翻译插件翻一块时从 SourceDocument.language 拿到解析这份源码的语言插件,不直接依赖某个语言插件。
|
||||
@@ -347,6 +385,8 @@ export interface LanguageAdapter {
|
||||
tokenRole: (leaf: Node) => TokenRole
|
||||
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
|
||||
call?: (node: Node) => CallFacts | undefined
|
||||
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
|
||||
displayNames?: DisplayNameReader
|
||||
}
|
||||
@@ -384,6 +424,11 @@ export interface SourceDocument {
|
||||
language: LanguageAdapter
|
||||
/** 写在注释里给代码阅读看的记号(中文名记号,见 DisplayNameFacts.markers):不是说明文字,翻注释、说明时去掉 */
|
||||
markers: SourceRange[]
|
||||
/**
|
||||
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
|
||||
* 排视图数据是同步的,这里只交出已经查到的;还没查到、查不到、宿主不查(没给)都是 undefined,查到了宿主重排、重发。
|
||||
*/
|
||||
calleeKind?: (call: CallFacts) => BlockKind | undefined
|
||||
}
|
||||
|
||||
/** 筛选属性和值的显示名,筛选栏、块的种类(图标的悬停提示)用它;没给名字的显示 id。 */
|
||||
@@ -401,8 +446,8 @@ export interface Translator {
|
||||
languageIds: string[]
|
||||
/** 译文用的语言(BCP 47 的语言部分,例如 'zh'):库规则按它挑自己的词表。 */
|
||||
displayLanguage: string
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, document: SourceDocument) => PreviewText
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文,跨几行写的调用连着摊开实参的表。 */
|
||||
translate: (block: TextBlock, document: SourceDocument) => TextTranslation
|
||||
/** 函数、类、枚举、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
|
||||
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { RETRY_DELAYS } from './nameResolution'
|
||||
|
||||
/** 排一遍视图数据用的:同步地问(交出已经查到的),和这一遍问了哪些问题。 */
|
||||
export interface AnswerSession<Q, A> {
|
||||
ask: (question: Q) => A | undefined
|
||||
questions: () => Q[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查一个问题:查到了是答案,查了没有是 null;语言扩展没答(还没起来)是 undefined,已知的留着不动,隔一会儿再查。
|
||||
*/
|
||||
export type Answer<Q, A> = (question: Q) => Promise<A | null | undefined>
|
||||
|
||||
/**
|
||||
* 一份打开着的文档里,排视图数据时要问、却要异步才查得到的问题(库规则问的名字的定义、调用的名字定义成哪种块)的答案,
|
||||
* 按问题的 id 记,id 一样的问题按一个查。这里不碰 VS Code。
|
||||
* 排视图数据是同步的:先交出已经查到的(文档改了也照旧用上一版查到的),记下这一遍问了什么;发出预览后再查,
|
||||
* 每个版本同一个问题查一次(语言扩展没答的照 RETRY_DELAYS 隔一会儿再查),查到的和已知的不一样就 changed,宿主按新结果重排、重发。
|
||||
*/
|
||||
export class DeferredAnswers<Q, A> {
|
||||
/** 查到的:查了没有是 null */
|
||||
private readonly known = new Map<string, A | null>()
|
||||
private version: number | undefined
|
||||
/** 眼下这个版本查过(或正在查)的问题 */
|
||||
private readonly checked = new Set<string>()
|
||||
private timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
constructor(
|
||||
private readonly idOf: (question: Q) => string,
|
||||
/** 两个答案对问的人来说一样不一样 */
|
||||
private readonly same: (a: A | null | undefined, b: A | null | undefined) => boolean,
|
||||
private readonly changed: () => void,
|
||||
private readonly delays: readonly number[] = RETRY_DELAYS,
|
||||
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
|
||||
) {}
|
||||
|
||||
session(): AnswerSession<Q, A> {
|
||||
const asked = new Map<string, Q>()
|
||||
return {
|
||||
ask: (question) => {
|
||||
const id = this.idOf(question)
|
||||
if (!asked.has(id))
|
||||
asked.set(id, question)
|
||||
return this.known.get(id) ?? undefined
|
||||
},
|
||||
questions: () => [...asked.values()],
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版问的问题:这个版本查过的不再查。 */
|
||||
resolve(version: number, questions: readonly Q[], answer: Answer<Q, A>): void {
|
||||
if (this.version !== version) {
|
||||
clearTimeout(this.timer)
|
||||
this.version = version
|
||||
this.checked.clear()
|
||||
}
|
||||
const fresh = questions.filter(question => !this.checked.has(this.idOf(question)))
|
||||
for (const question of fresh)
|
||||
this.checked.add(this.idOf(question))
|
||||
if (fresh.length === 0)
|
||||
return
|
||||
const attempt = (pending: readonly Q[], tries: number): void => {
|
||||
void this.check(pending, answer).then(({ unanswered, changed }) => {
|
||||
if (this.version !== version)
|
||||
return
|
||||
if (changed)
|
||||
this.changed()
|
||||
if (unanswered.length > 0 && tries < this.delays.length)
|
||||
this.timer = this.schedule(() => attempt(unanswered, tries + 1), this.delays[tries])
|
||||
})
|
||||
}
|
||||
attempt(fresh, 0)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.version = undefined
|
||||
}
|
||||
|
||||
/** 查一遍:没答的问题留着已知的不动,交回去等下一次再查。 */
|
||||
private async check(questions: readonly Q[], answer: Answer<Q, A>): Promise<{ unanswered: Q[], changed: boolean }> {
|
||||
const unanswered: Q[] = []
|
||||
let changed = false
|
||||
await Promise.all(questions.map(async (question) => {
|
||||
const found = await answer(question)
|
||||
if (found === undefined) {
|
||||
unanswered.push(question)
|
||||
return
|
||||
}
|
||||
const id = this.idOf(question)
|
||||
if (!this.same(this.known.get(id), found))
|
||||
changed = true
|
||||
this.known.set(id, found)
|
||||
}))
|
||||
return { unanswered, changed }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Location, LocationLink, Position } from 'vscode'
|
||||
import type { RuleLocation } from '../contract'
|
||||
import type { BlockKind, LanguageAdapter, RuleLocation } from '../contract'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DefinitionTarget } from './definitionAction'
|
||||
import { commands, Range, Uri, window, workspace } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { readDefinitionKind } from '../blockView'
|
||||
|
||||
/** 查到的一处定义:文件和定义处那个名字的范围。 */
|
||||
export interface FoundLocation { uri: Uri, range: Range }
|
||||
@@ -36,6 +38,23 @@ export function ruleDefinitions(locations: readonly RuleLocation[]): FoundLocati
|
||||
return locations.map(({ uri, start, end }) => ({ uri: Uri.parse(uri), range: new Range(start.line, start.character, end.line, end.character) }))
|
||||
}
|
||||
|
||||
/** 定义处那个名字定义的是哪种块:打开目标文件,用它那种语言的语言插件分块认;目标打不开、语言不支持、不在哪块的名字上时没有。 */
|
||||
export async function definitionKind(
|
||||
location: FoundLocation,
|
||||
languageFor: (languageId: string) => LanguageAdapter | undefined,
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
): Promise<BlockKind | undefined> {
|
||||
try {
|
||||
const document = await workspace.openTextDocument(location.uri)
|
||||
const adapter = languageFor(document.languageId)
|
||||
return adapter && await readDefinitionKind(runtime, adapter, document.languageId, document.getText(), document.offsetAt(location.range.start))
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `认不出 ${location.uri.toString()} 里定义的是哪种块:${err instanceof Error ? err.message : String(err)}` })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 查到的各处定义打开目标文件补上行文字、下标;目标打不开成文本的(例如 Python 命名空间包指向的目录)略过。 */
|
||||
export async function findDefinitions(locations: readonly FoundLocation[], supports: (languageId: string) => boolean): Promise<Definition[]> {
|
||||
const found = await Promise.all(locations.map(async (location): Promise<Definition[]> => {
|
||||
|
||||
@@ -118,6 +118,21 @@ describe('预览的 document 消息', () => {
|
||||
expect(reports).toEqual(['库规则 fastapi 给筛选属性起名时出错,筛选栏照写 id:词表坏了'])
|
||||
})
|
||||
|
||||
it('调用的名字定义成哪种块交给 calleeKind 问:跨几行新建对象的一行译文下面带实参表', async () => {
|
||||
const source = 'def f(user):\n return Detail(\n id=user.id,\n name=user.name,\n )\n'
|
||||
const asked: string[] = []
|
||||
const { message } = await documentMessage(doc('python', source), registryWithPython(), runtime, ignore, undefined, (call) => {
|
||||
asked.push(call.callee.text)
|
||||
return 'class'
|
||||
})
|
||||
expect(asked).toEqual(['Detail'])
|
||||
expect(message.blocks[0].children[0]).toMatchObject({
|
||||
kind: 'return',
|
||||
translation: [{ text: '返回' }, { text: ' ' }, { text: '新建' }, { text: ' ' }, { text: 'Detail', role: 'type' }, { text: ' ' }, { text: '· 2 个参数', role: 'hint' }],
|
||||
table: { columns: ['参数', '值'], items: [{ cells: [[{ text: 'id' }], expect.anything()] }, { cells: [[{ text: 'name' }], expect.anything()] }] },
|
||||
})
|
||||
})
|
||||
|
||||
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
|
||||
const broken: LibraryRule = {
|
||||
id: 'broken',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DisplayName, RuleContext } from '../contract'
|
||||
import type { DisplayName, RuleContext, SourceDocument } from '../contract'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { DocumentMessage } from './messages'
|
||||
@@ -21,7 +21,8 @@ export interface DocumentRead {
|
||||
|
||||
/**
|
||||
* 当前文档 → 发给预览的 document 消息;语言不支持或解析失败时带 error,blocks 为空。库规则出错不算失败,交给 report 记下;
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup)。筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
|
||||
* 规则查名字的定义时问 lookup(RuleContext.lookup),翻译插件问调用的名字定义成哪种块时问 calleeKind(SourceDocument.calleeKind)。
|
||||
* 筛选属性和值的名字先取翻译插件的,没给的再取库规则的(LibraryRule.attributeNames)。
|
||||
*/
|
||||
export async function documentMessage(
|
||||
document: DocumentSnapshot,
|
||||
@@ -29,6 +30,7 @@ export async function documentMessage(
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>,
|
||||
report: (message: string) => void,
|
||||
lookup?: RuleContext['lookup'],
|
||||
calleeKind?: SourceDocument['calleeKind'],
|
||||
): Promise<DocumentRead> {
|
||||
const { languageId, version } = document
|
||||
const source = document.getText()
|
||||
@@ -48,7 +50,7 @@ export async function documentMessage(
|
||||
return { message: { ...message, error: `代码阅读还不支持「${languageId}」这种语言` }, displayNames: [] }
|
||||
try {
|
||||
const rules = registry.rulesFor(languageId)
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup })
|
||||
const { blocks, displayNames } = await readBlockViews(runtime, adapter, translator, languageId, source, { rules, report, lookup }, calleeKind)
|
||||
const names = mergeAttributeNames([translator?.attributeNames, ...ruleAttributeNames(rules, translator?.displayLanguage, report)])
|
||||
return { message: { ...message, blocks, attributes: fileAttributes(blocks, names) }, displayNames }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, Uri, WebviewPanel } from 'vscode'
|
||||
import type { ClaudeImage } from '../../cc/spawnClaude'
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
|
||||
import type { BlockKind, RuleLocation, RuleReference, WorkspaceIndex } from '../contract'
|
||||
import type { DefinitionStart, IndexedName, NameSources } from '../nameResolution'
|
||||
import type { CodeQa } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
@@ -14,13 +14,14 @@ import type { ExtensionToWebview, Reveal, WebviewToExtension } from './messages'
|
||||
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { webviewHtml, webviewUiRoot } from '../../webview/html'
|
||||
import { DocumentCallees } from '../calleeKinds'
|
||||
import { blockReplacement } from '../editText'
|
||||
import { locate } from '../folderStore'
|
||||
import { DocumentNames, resolveNames } from '../nameResolution'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
@@ -131,6 +132,8 @@ class Preview {
|
||||
private readonly names: DocumentNames
|
||||
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
|
||||
private readonly lookups: DocumentLookups
|
||||
/** 调用的名字定义成哪种块(分新建还是调用):查到的变了就重发文档 */
|
||||
private readonly callees: DocumentCallees
|
||||
|
||||
constructor(
|
||||
private readonly document: TextDocument,
|
||||
@@ -154,6 +157,7 @@ class Preview {
|
||||
this.post({ type: 'displayNames', version, names })
|
||||
})
|
||||
this.lookups = new DocumentLookups(() => this.schedulePush())
|
||||
this.callees = new DocumentCallees(() => this.schedulePush())
|
||||
this.disposables = [
|
||||
panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)),
|
||||
panel.onDidChangeViewState(() => {
|
||||
@@ -223,9 +227,10 @@ class Preview {
|
||||
const { languageId, version } = this.document
|
||||
const source = this.document.getText()
|
||||
const session = this.lookups.session()
|
||||
const callees = this.callees.session()
|
||||
const { message, displayNames } = await documentMessage({ languageId, version, getText: () => source }, this.registry, this.runtime, (text) => {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `${this.document.uri.fsPath}:${text}` })
|
||||
}, session.lookup)
|
||||
}, session.lookup, callees.calleeKind)
|
||||
if (request !== this.latest || !this.ready)
|
||||
return
|
||||
this.definitions.remember(message.version, message.blocks)
|
||||
@@ -237,9 +242,24 @@ class Preview {
|
||||
if (version === this.document.version) {
|
||||
this.names.resolve(version, report => resolveNames(displayNames, message.blocks, this.nameSources(), report))
|
||||
this.lookups.resolve(version, session.requests(), this.lookupSources())
|
||||
this.callees.resolve(version, callees.requests(), offset => this.calleeKind(offset))
|
||||
}
|
||||
}
|
||||
|
||||
/** 这份文档里 offset 处名字的定义是哪种块:语言扩展查到的几处定义里第一处认得出的;语言扩展没答是 undefined。 */
|
||||
private async calleeKind(offset: number): Promise<BlockKind | null | undefined> {
|
||||
const { document } = this
|
||||
const found = await languageDefinitions(document.uri, document.positionAt(offset))
|
||||
if (found.length === 0)
|
||||
return undefined
|
||||
for (const location of found) {
|
||||
const kind = await definitionKind(location, languageId => this.registry.languageFor(languageId), this.runtime)
|
||||
if (kind)
|
||||
return kind
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 这份文档里 offset 处名字的定义(语言扩展查的)落在哪儿。 */
|
||||
private async definitionStarts(offset: number): Promise<DefinitionStart[]> {
|
||||
const { document } = this
|
||||
@@ -284,6 +304,7 @@ class Preview {
|
||||
clearTimeout(this.timer)
|
||||
this.names.dispose()
|
||||
this.lookups.dispose()
|
||||
this.callees.dispose()
|
||||
for (const disposable of this.disposables)
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { window } from 'vscode'
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
|
||||
export async function removeWorktree(panel: Panel, relPath: string, issueNumber: number): Promise<void> {
|
||||
const state = await panel.readIssueState(issueNumber)
|
||||
const branch = typeof state.branch === 'string' ? state.branch : ''
|
||||
const choice = await window.showWarningMessage(
|
||||
branch ? `确认删除 worktree ${relPath} 及其分支 ${branch}?` : `确认删除 worktree ${relPath}?`,
|
||||
{ modal: true },
|
||||
'删除',
|
||||
)
|
||||
if (choice !== '删除')
|
||||
return
|
||||
}
|
||||
|
||||
class Preview {
|
||||
constructor(document: TextDocument, owner: Owner) {
|
||||
this.definitions = new DefinitionSources({
|
||||
rule: async reference => ruleDefinitions(await owner.resolveReference(reference)),
|
||||
language: offset => languageDefinitions(document.uri, document.positionAt(offset)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function commentsUrl(opts: Options): URL {
|
||||
const url = new URL(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/issues/${opts.index}/comments`,
|
||||
)
|
||||
return url
|
||||
}
|
||||
|
||||
export function merged(base: string[], extra: string[]): string[] {
|
||||
return concatAll(
|
||||
base,
|
||||
...extra,
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IterationFacts, Node } from '../contract'
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { factsByLine, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { factsByLine, nodeFacts, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createPythonAdapter } from './python'
|
||||
|
||||
const adapter = createPythonAdapter(WASM_DIR)
|
||||
@@ -488,3 +488,28 @@ describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
expect(await iterations('x = [1, 2]\ny = {"a": 1}\nz = (a, b)\nfor x in y:\n pass')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 调用', () => {
|
||||
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.call?.(node))
|
||||
|
||||
it('实参照源码顺序:按位置、按名字、*x、**x;新建对象和调用写法一样,不给 construct;点分名字取最后一节', async () => {
|
||||
const source = 'user_service.update_user(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)'
|
||||
expect(await calls(source)).toEqual([{
|
||||
callee: 'user_service.update_user',
|
||||
name: 'update_user',
|
||||
argumentList: '(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)',
|
||||
arguments: [
|
||||
{ kind: 'positional', value: 'db' },
|
||||
{ kind: 'keyword', name: 'user_id', value: 'user_id' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{ kind: 'keywordSpread', value: 'options' },
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
it('唯一的实参是生成器表达式时就是它;被调用的不是名字时没有 name;装饰器、父类列表不是调用', async () => {
|
||||
expect(await calls('sum(x for x in xs)')).toEqual([{ callee: 'sum', name: 'sum', argumentList: '(x for x in xs)', arguments: [{ kind: 'positional', value: '(x for x in xs)' }] }])
|
||||
expect(await calls('handlers[k]()')).toEqual([{ callee: 'handlers[k]', argumentList: '()', arguments: [] }])
|
||||
expect(await calls('@cache\nclass A(Base):\n pass')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, 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'
|
||||
@@ -363,6 +363,42 @@ function pythonIteration(node: Node): IterationFacts | undefined {
|
||||
}
|
||||
|
||||
/** elif / else 展平成紧跟 if 的兄弟块。 */
|
||||
/** 调用的实参:name=value 按名字传,*x、**x 展开,别的按位置传;唯一的实参是生成器表达式(f(x for x in y))时就是它。 */
|
||||
function callArgument(arg: Node): CallArgument | undefined {
|
||||
switch (arg.type) {
|
||||
case 'keyword_argument': {
|
||||
const name = arg.childForFieldName('name')
|
||||
const value = arg.childForFieldName('value')
|
||||
return name && value ? { kind: 'keyword', name, value } : undefined
|
||||
}
|
||||
case 'list_splat':
|
||||
case 'dictionary_splat': {
|
||||
const [value] = syntaxChildren(arg)
|
||||
if (!value)
|
||||
return undefined
|
||||
return { kind: arg.type === 'list_splat' ? 'spread' : 'keywordSpread', value }
|
||||
}
|
||||
default:
|
||||
return { kind: 'positional', value: arg }
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用的结构:新建对象和调用函数写法一样,不给 construct。被调用的是名字或点分名字时 name 是最后那个名字。 */
|
||||
function pythonCall(node: Node): CallFacts | undefined {
|
||||
const callee = node.type === 'call' ? node.childForFieldName('function') : null
|
||||
const list = node.childForFieldName('arguments')
|
||||
if (!callee || !list)
|
||||
return undefined
|
||||
const name = callee.type === 'identifier' ? callee : callee.type === 'attribute' ? callee.childForFieldName('attribute') : null
|
||||
const args = list.type === 'generator_expression' ? [list] : syntaxChildren(list)
|
||||
return {
|
||||
callee,
|
||||
...(name && { name }),
|
||||
argumentList: list,
|
||||
arguments: args.map(callArgument).filter(arg => arg !== undefined),
|
||||
}
|
||||
}
|
||||
|
||||
function ifBlocks(node: Node): CodeBlock[] {
|
||||
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
|
||||
for (const alternative of node.childrenForFieldName('alternative')) {
|
||||
@@ -603,6 +639,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
|
||||
tokenRole: pythonTokenRole,
|
||||
iteration: pythonIteration,
|
||||
call: pythonCall,
|
||||
displayNames: pythonNameReader,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { factsByLine, nodeFacts, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { createTypeScriptAdapter } from './typescript'
|
||||
|
||||
const adapter = createTypeScriptAdapter(WASM_DIR)
|
||||
@@ -612,3 +612,23 @@ describe('typeScript 枚举', () => {
|
||||
expect(byLine[1]).toMatchObject({ members: [{ name: 'A', docs: [] }, { name: 'B', docs: ['/** 第二 */'] }, { name: 'C', docs: ['// 第三'] }] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 调用', () => {
|
||||
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.call?.(node))
|
||||
|
||||
it('new X(…) 是新建,被调用的不含 new;别的调用不是新建;...x 是展开;点分名字取最后一节', async () => {
|
||||
expect(await calls('new vscode.Uri(a, ...rest)')).toEqual([{
|
||||
callee: 'vscode.Uri',
|
||||
name: 'Uri',
|
||||
construct: true,
|
||||
argumentList: '(a, ...rest)',
|
||||
arguments: [{ kind: 'positional', value: 'a' }, { kind: 'spread', value: 'rest' }],
|
||||
}])
|
||||
expect(await calls('run<T>({ a: 1 })')).toEqual([{ callee: 'run', name: 'run', construct: false, argumentList: '({ a: 1 })', arguments: [{ kind: 'positional', value: '{ a: 1 }' }] }])
|
||||
})
|
||||
|
||||
it('带标签的模板字符串、不带括号的 new X 不是调用', async () => {
|
||||
expect(await calls('sql`select 1`')).toEqual([])
|
||||
expect(await calls('new Date')).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, DeclaratorFacts, 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 { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
@@ -542,6 +542,26 @@ const LITERAL_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this',
|
||||
const STRING_NODES = new Set(['string', 'template_string', 'regex'])
|
||||
const FUNCTION_NAMED = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'function', 'generator_function', 'method_definition', 'method_signature', 'abstract_method_signature', 'function_signature'])
|
||||
|
||||
/** 调用的结构:`new X(…)` 是新建对象,别的调用不是;带标签的模板字符串(sql`…`)、不带括号的 `new X` 不算。 */
|
||||
function typescriptCall(node: Node): CallFacts | undefined {
|
||||
const construct = node.type === 'new_expression'
|
||||
const callee = construct ? node.childForFieldName('constructor') : node.type === 'call_expression' ? node.childForFieldName('function') : null
|
||||
const list = node.childForFieldName('arguments')
|
||||
if (!callee || list?.type !== 'arguments')
|
||||
return undefined
|
||||
const name = callee.type === 'identifier' ? callee : callee.type === 'member_expression' ? callee.childForFieldName('property') : null
|
||||
return {
|
||||
callee,
|
||||
...(name && { name }),
|
||||
construct,
|
||||
argumentList: list,
|
||||
arguments: syntaxChildren(list).map((arg): CallArgument => {
|
||||
const spread = arg.type === 'spread_element' ? syntaxChildren(arg)[0] : undefined
|
||||
return spread ? { kind: 'spread', value: spread } : { kind: 'positional', value: arg }
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function insideString(leaf: Node): boolean {
|
||||
for (let node = leaf.parent; node; node = node.parent) {
|
||||
if (node.type === 'template_substitution')
|
||||
@@ -620,6 +640,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
},
|
||||
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
|
||||
tokenRole: typescriptTokenRole,
|
||||
call: typescriptCall,
|
||||
displayNames: typescriptNameReader,
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,13 @@ describe('中文名落到哪些名字上', () => {
|
||||
expect(rendered.get(at(ROUTES, 'delete_item'))).toBe('delete_item')
|
||||
})
|
||||
|
||||
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)里带下标的名字也算', async () => {
|
||||
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
|
||||
const rendered = renderedNames((await read(source)).blocks)
|
||||
expect(rendered.get(at(source, 'id='))).toBe('id')
|
||||
expect(rendered.get(at(source, 'user.id'))).toBe('user')
|
||||
})
|
||||
|
||||
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
|
||||
const { blocks, displayNames } = await read(ROUTES)
|
||||
const definition = at(ROUTES, 'current_user')
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ShownName {
|
||||
definition?: true
|
||||
}
|
||||
|
||||
/** 视图数据里照抄自源码、带下标的名字(一行译文、属性表的值、各层子行的格子、框里):下标 → 原文。 */
|
||||
/** 视图数据里照抄自源码、带下标的名字(一行译文和它下面的表、属性表的值、各层子行的格子、框里):下标 → 原文。 */
|
||||
export function renderedNames(blocks: readonly BlockView[]): Map<number, string> {
|
||||
const found = new Map<number, string>()
|
||||
const inline = (pieces: readonly Inline[]): void => {
|
||||
@@ -46,6 +46,7 @@ export function renderedNames(blocks: readonly BlockView[]): Map<number, string>
|
||||
}
|
||||
else {
|
||||
text(block.translation)
|
||||
items(block.table?.items)
|
||||
}
|
||||
visit(block.children)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ function adapter(id: string, languageIds: string[]): LanguageAdapter {
|
||||
}
|
||||
|
||||
function translator(id: string, languageIds: string[]): Translator {
|
||||
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => [], describe: () => [], expression: () => [], doc: () => undefined }
|
||||
return { id, label: id, languageIds, displayLanguage: 'zh', translate: () => ({ text: [] }), describe: () => [], expression: () => [], doc: () => undefined }
|
||||
}
|
||||
|
||||
function rule(id: string, languageIds: string[]): LibraryRule {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IndexEntry, RuleContext, WorkspaceIndex } from './contract'
|
||||
import type { DefinitionStart } from './nameResolution'
|
||||
import type { IndexedEntry } from './workspaceIndex'
|
||||
import { RETRY_DELAYS } from './nameResolution'
|
||||
import { DeferredAnswers } from './deferredAnswers'
|
||||
|
||||
/**
|
||||
* 库规则认领时查的名字(RuleContext.lookup):名字的定义在规则的工作区索引里是哪一项。
|
||||
@@ -33,91 +33,44 @@ function same(a: IndexEntry | null | undefined, b: IndexEntry | null | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 一份打开着的文档里库规则问的名字查到了什么:按「索引 id + 名字原文」记,同一份文档里同名的名字按一处查。
|
||||
* 排视图数据是同步的:先交出已经查到的(文档改了也照旧用上一版查到的),记下这一遍问了什么;发出预览后再查,
|
||||
* 每个版本同一个名字查一次(索引里有同名的项、语言扩展却没答时照 RETRY_DELAYS 隔一会儿再查),查到的和已知的不一样就 changed,
|
||||
* 宿主按新结果重排、重发。
|
||||
* 一份打开着的文档里库规则问的名字查到了什么:按「索引 id + 名字原文」记,同一份文档里同名的名字按一处查;
|
||||
* 什么时候查、查到了怎么叫宿主重排见 DeferredAnswers。
|
||||
*/
|
||||
export class DocumentLookups {
|
||||
/** 查到的:没有这个定义是 null */
|
||||
private readonly known = new Map<string, IndexEntry | null>()
|
||||
private version: number | undefined
|
||||
/** 眼下这个版本查过(或正在查)的名字 */
|
||||
private readonly checked = new Set<string>()
|
||||
private timer: ReturnType<typeof setTimeout> | undefined
|
||||
private readonly answers: DeferredAnswers<LookupRequest, IndexEntry>
|
||||
|
||||
constructor(
|
||||
private readonly changed: () => void,
|
||||
private readonly delays: readonly number[] = RETRY_DELAYS,
|
||||
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
|
||||
) {}
|
||||
constructor(changed: () => void, delays?: readonly number[], schedule?: (run: () => void, ms: number) => ReturnType<typeof setTimeout>) {
|
||||
this.answers = new DeferredAnswers(({ index, key }) => idOf(index, key), same, changed, delays, schedule)
|
||||
}
|
||||
|
||||
session(): LookupSession {
|
||||
const requests = new Map<string, LookupRequest>()
|
||||
const session = this.answers.session()
|
||||
return {
|
||||
lookup: (index, name) => {
|
||||
const id = idOf(index, name.text)
|
||||
if (!requests.has(id))
|
||||
requests.set(id, { index, key: name.text, offset: name.startIndex })
|
||||
return this.known.get(id) ?? undefined
|
||||
},
|
||||
requests: () => [...requests.values()],
|
||||
lookup: (index, name) => session.ask({ index, key: name.text, offset: name.startIndex }),
|
||||
requests: session.questions,
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 version 这一版规则问的名字:这个版本查过的不再查。 */
|
||||
resolve(version: number, requests: readonly LookupRequest[], sources: LookupSources): void {
|
||||
if (this.version !== version) {
|
||||
clearTimeout(this.timer)
|
||||
this.version = version
|
||||
this.checked.clear()
|
||||
}
|
||||
const fresh = requests.filter(request => !this.checked.has(idOf(request.index, request.key)))
|
||||
for (const request of fresh)
|
||||
this.checked.add(idOf(request.index, request.key))
|
||||
if (fresh.length === 0)
|
||||
return
|
||||
const attempt = (tries: number): void => {
|
||||
void this.check(fresh, sources).then(({ settled, changed }) => {
|
||||
if (this.version !== version)
|
||||
return
|
||||
if (changed)
|
||||
this.changed()
|
||||
if (!settled && tries < this.delays.length)
|
||||
this.timer = this.schedule(() => attempt(tries + 1), this.delays[tries])
|
||||
})
|
||||
}
|
||||
attempt(0)
|
||||
this.answers.resolve(version, requests, request => find(request, sources))
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
clearTimeout(this.timer)
|
||||
this.version = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 查一遍:索引里没有同名的项就是没有这个定义;有就问语言扩展,定义处正是其中一项才算。
|
||||
* 语言扩展没答(还没起来)的名字留着已知的不动,算没查完(settled 为 false)。
|
||||
*/
|
||||
private async check(requests: readonly LookupRequest[], sources: LookupSources): Promise<{ settled: boolean, changed: boolean }> {
|
||||
let settled = true
|
||||
let changed = false
|
||||
await Promise.all(requests.map(async ({ index, key, offset }) => {
|
||||
const id = idOf(index, key)
|
||||
const candidates = await sources.find(index, key)
|
||||
let found: IndexEntry | null = null
|
||||
if (candidates.length > 0) {
|
||||
const definitions = await sources.definitions(offset)
|
||||
if (definitions.length === 0) {
|
||||
settled = false
|
||||
return
|
||||
}
|
||||
found = candidates.find(({ location }) => definitions.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))?.entry ?? null
|
||||
}
|
||||
if (!same(this.known.get(id), found))
|
||||
changed = true
|
||||
this.known.set(id, found)
|
||||
}))
|
||||
return { settled, changed }
|
||||
this.answers.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查规则问的一个名字:索引里没有同名的项就是没有这个定义;有就问语言扩展,定义处正是其中一项才算。
|
||||
* 语言扩展没答(还没起来)是 undefined。
|
||||
*/
|
||||
async function find({ index, key, offset }: LookupRequest, sources: LookupSources): Promise<IndexEntry | null | undefined> {
|
||||
const candidates = await sources.find(index, key)
|
||||
if (candidates.length === 0)
|
||||
return null
|
||||
const definitions = await sources.definitions(offset)
|
||||
if (definitions.length === 0)
|
||||
return undefined
|
||||
return candidates.find(({ location }) => definitions.some(at => at.uri === location.uri && at.line === location.start.line && at.character === location.start.character))?.entry ?? null
|
||||
}
|
||||
@@ -82,6 +82,28 @@ export function nodesToText(value: unknown): unknown {
|
||||
return value
|
||||
}
|
||||
|
||||
/** 源码里 read 认得出的每个节点(先序,外层在前)读出的结构,节点换成源码。 */
|
||||
export async function nodeFacts(adapter: LanguageAdapter, languageId: string, source: string, read: (node: Node) => unknown): Promise<unknown[]> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
const found: unknown[] = []
|
||||
const visit = (node: Node): void => {
|
||||
const facts = read(node)
|
||||
if (facts)
|
||||
found.push(nodesToText(facts))
|
||||
for (const child of node.children) {
|
||||
if (child)
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(tree.rootNode)
|
||||
return found
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** 每个属性表块的 facts(节点换成源码),按 header 起始行(1 起)索引。 */
|
||||
export function factsByLine(blocks: CodeBlock[]): Record<number, unknown> {
|
||||
const byLine: Record<number, unknown> = {}
|
||||
|
||||
@@ -53,8 +53,8 @@ const translator = createZhTemplateTranslator({
|
||||
|
||||
function translateFirst(language: LanguageAdapter, source: string): Promise<string> {
|
||||
return withBlocks(language, 'fake', source, ([block]) => {
|
||||
const translation = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in translation ? translation.markdown : segmentText(translation)
|
||||
const { text } = translator.translate(block as TextBlock, { source, language, markers: [] })
|
||||
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Inline, LanguageAdapter, PropertyItem, PropertyRow, Segment, TokenRole, Translator } from '../contract'
|
||||
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
|
||||
import type { ZhTable } from './zhTemplate'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
@@ -11,34 +11,37 @@ import { createZhTemplateTranslator } from './zhTemplate'
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Inline[], markdown?: string }
|
||||
/** 一块的译文:一行译文在 segments 里,下面摊开实参的表在 table 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||
interface Translated { kind: string, segments: Inline[], markdown?: string, table?: PreviewTable }
|
||||
|
||||
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||
/**
|
||||
* 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。
|
||||
* calleeKind 同 SourceDocument.calleeKind(调用的名字定义成哪种块),不给就是宿主不查。
|
||||
*/
|
||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, Translated>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, Translated>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (byLine.has(line) || 'facts' in block)
|
||||
return
|
||||
const translation = translator.translate(block, { source, language: adapter, markers: [] })
|
||||
const { text: translation, table } = translator.translate(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) })
|
||||
byLine.set(line, 'markdown' in translation
|
||||
? { kind: block.kind, segments: [], markdown: translation.markdown }
|
||||
: { kind: block.kind, segments: translation })
|
||||
: { kind: block.kind, segments: translation, ...(table && { table }) })
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
}
|
||||
|
||||
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引。 */
|
||||
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, PropertyRow[]>> {
|
||||
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引;calleeKind 同 translateAll。 */
|
||||
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, calleeKind?: SourceDocument['calleeKind']): Promise<Map<number, PropertyRow[]>> {
|
||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||
const byLine = new Map<number, PropertyRow[]>()
|
||||
walk(blocks, (block) => {
|
||||
const line = block.header.startRow + 1
|
||||
if (!byLine.has(line) && 'facts' in block)
|
||||
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [] }))
|
||||
byLine.set(line, translator.describe(block, { source, language: adapter, markers: [], ...(calleeKind && { calleeKind }) }))
|
||||
})
|
||||
return byLine
|
||||
})
|
||||
@@ -165,12 +168,12 @@ describe('typeScript 中文直译', () => {
|
||||
|
||||
it('多行合成一行、去掉右括号前的尾逗号、行中注释不进译文', async () => {
|
||||
const source = [
|
||||
'call(',
|
||||
'list = [',
|
||||
' first, // 第一个',
|
||||
' [],',
|
||||
')',
|
||||
']',
|
||||
].join('\n')
|
||||
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('call( first, 空数组 )')
|
||||
expect(texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))[1]).toBe('list = [ first, 空数组 ]')
|
||||
})
|
||||
|
||||
it('as / in / extends / default 按所在语法节点换译法', async () => {
|
||||
@@ -417,8 +420,10 @@ describe('python 写在表达式里的遍历(推导式)', () => {
|
||||
5: '⟦集合|遍历 pairs 的每个 n, _,取 n⟧',
|
||||
6: '⟦列表|遍历 grid 的每个 row,取 ⟦列表|遍历 row 的每个 y,取 y⟧⟧',
|
||||
7: 'pick(a, ⟦生成器|遍历 b 的每个 y,取 y⟧)',
|
||||
8: 'sum(⟦生成器|遍历 counts 的每个 n,取 n⟧)',
|
||||
8: '调用 sum · 1 个参数',
|
||||
})
|
||||
// 跨几行写的调用唯一的实参是生成器:共用的那对括号属于调用,实参表里只有框
|
||||
expect(propertyLines(map.get(8) ?? []).at(-1)).toBe(' 第 1 个 | ⟦生成器|遍历 counts 的每个 n,取 n⟧')
|
||||
const async = valueOf(map.get(3), '初始值')[0]
|
||||
expect('segments' in async && async.segments.filter(piece => 'role' in piece && piece.role === 'keyword')).toEqual([
|
||||
{ text: '异步遍历', role: 'keyword' },
|
||||
@@ -1028,6 +1033,173 @@ describe('源码下标(跳到定义用)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('跨几行写的调用', () => {
|
||||
const routes = readFixture('routes.py')
|
||||
const calls = readFixture('calls.ts.txt')
|
||||
|
||||
/** 调用的名字定义成哪种块(按被调用的表达式原文查,没有的是还没查到),并记下问了哪些。 */
|
||||
function kinds(known: Record<string, BlockKind>): { asked: string[], calleeKind: (call: CallFacts) => BlockKind | undefined } {
|
||||
const asked: string[] = []
|
||||
return {
|
||||
asked,
|
||||
calleeKind: (call) => {
|
||||
asked.push(call.callee.text)
|
||||
return known[call.callee.text]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function tableLines(table: PreviewTable | undefined): string[] {
|
||||
return (table?.items ?? []).map(item => item.cells.map(segmentText).join(' | '))
|
||||
}
|
||||
|
||||
const USER_DETAIL = [
|
||||
'id | user.id',
|
||||
'username | user.username',
|
||||
'display_name | user.display_name',
|
||||
'role | user.role.value',
|
||||
'email | user.email',
|
||||
'phone | user.phone',
|
||||
'bio | user.bio',
|
||||
'avatar_url | user.avatar_url',
|
||||
'is_active | user.is_active',
|
||||
'created_at | user.created_at',
|
||||
'updated_at | user.updated_at',
|
||||
]
|
||||
|
||||
it('返回里新建对象(名字定义成类):一行「返回 新建 UserDetailResponse · 11 个参数」,实参摊成表(参数 | 值),按名字传的写名字', async () => {
|
||||
const { asked, calleeKind } = kinds({ UserDetailResponse: 'class' })
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
|
||||
expect(colors(line?.segments)).toEqual([
|
||||
{ text: '返回', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '新建', role: 'storage' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'UserDetailResponse', role: 'type' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '· 11 个参数', role: 'hint' },
|
||||
])
|
||||
expect(line?.table?.columns).toEqual(['参数', '值'])
|
||||
expect(tableLines(line?.table)).toEqual(USER_DETAIL)
|
||||
expect(asked).toContain('UserDetailResponse')
|
||||
// 被调用的名字、按名字传的名字、值里的名字都带源码下标:能跳到定义、显示中文名
|
||||
const call = routes.indexOf('UserDetailResponse(')
|
||||
expect(line?.segments.find(piece => 'role' in piece && piece.role === 'type')).toMatchObject({ offset: call })
|
||||
expect(line?.table?.items[0].cells).toEqual([
|
||||
[{ text: 'id', role: 'variable', offset: routes.indexOf('id=user.id', call) }],
|
||||
[
|
||||
{ text: 'user', role: 'variable', offset: routes.indexOf('user.id', call) },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'id', role: 'variable', offset: routes.indexOf('user.id', call) + 5 },
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('名字的定义还没查到、查不到、是函数都按调用算:被调用的名字按函数着色', async () => {
|
||||
for (const calleeKind of [undefined, kinds({}).calleeKind, kinds({ UserDetailResponse: 'function' }).calleeKind]) {
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, routes, calleeKind)).get(45)
|
||||
expect(text(line?.segments)).toBe('返回 调用 UserDetailResponse · 11 个参数')
|
||||
expect(line?.segments.find(piece => 'role' in piece && piece.text === 'UserDetailResponse')).toMatchObject({ role: 'function' })
|
||||
}
|
||||
const exception = (await translateAll(py, 'python', pythonZhTranslator, routes, kinds({ UserDetailResponse: 'exception' }).calleeKind)).get(96)
|
||||
expect(text(exception?.segments)).toBe('返回 新建 UserDetailResponse · 11 个参数')
|
||||
})
|
||||
|
||||
it('变量的初始值里跨几行调用:初始值一行「等待 调用 user_service.update_user · 10 个参数」,实参表展开在这一行下面,按位置传的写「第 N 个」', async () => {
|
||||
const rows = (await describeAll(py, 'python', pythonZhTranslator, routes, kinds({}).calleeKind)).get(83) ?? []
|
||||
expect(propertyLines(rows)).toEqual([
|
||||
'名称: user',
|
||||
'初始值: 等待 调用 user_service.update_user · 10 个参数',
|
||||
' 第 1 个 | db',
|
||||
' user_id | user_id',
|
||||
' display_name | body.display_name',
|
||||
' email | body.email',
|
||||
' phone | body.phone',
|
||||
' bio | body.bio',
|
||||
' avatar_url | body.avatar_url',
|
||||
' role | body.role',
|
||||
' is_active | body.is_active',
|
||||
' password | body.password',
|
||||
])
|
||||
const value = rows.find(row => row.label === '初始值')
|
||||
expect(value?.columns).toEqual(['参数', '值'])
|
||||
expect(colors(value?.value)).toEqual([
|
||||
{ text: '等待', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '调用', role: 'storage' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'user_service', role: 'variable' },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'update_user', role: 'function' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: '· 10 个参数', role: 'hint' },
|
||||
])
|
||||
expect(value?.items?.[0].cells[0]).toEqual([{ text: '第 1 个', role: 'hint' }])
|
||||
})
|
||||
|
||||
it('一行写完的调用照旧一行译文,不问定义', async () => {
|
||||
const { asked, calleeKind } = kinds({})
|
||||
const map = await describeAll(py, 'python', pythonZhTranslator, routes, calleeKind)
|
||||
expect(propertyLines(map.get(43) ?? [])).toEqual(['名称: user', '初始值: 等待 user_service.get_user(db, user_id=user_id)'])
|
||||
expect(propertyLines(map.get(118) ?? [])).toEqual(['名称: payload', '初始值: 等待 file.read()'])
|
||||
expect(asked).not.toContain('user_service.get_user')
|
||||
expect(asked).not.toContain('file.read')
|
||||
})
|
||||
|
||||
it('*x 写「展开」、**x 写「展开关键字」,「第 N 个」只数按位置传的;实参里的调用不再排成表', async () => {
|
||||
const source = [
|
||||
'run(',
|
||||
' first,',
|
||||
' *rest,',
|
||||
' second,',
|
||||
' key=Inner(',
|
||||
' x=1,',
|
||||
' ),',
|
||||
' **options,',
|
||||
')',
|
||||
].join('\n')
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, source)).get(1)
|
||||
expect(text(line?.segments)).toBe('调用 run · 5 个参数')
|
||||
expect(tableLines(line?.table)).toEqual(['第 1 个 | first', '展开 | rest', '第 2 个 | second', 'key | Inner( x=1 )', '展开关键字 | options'])
|
||||
})
|
||||
|
||||
it('typeScript:new 写法就是新建,不带 new 的是调用,都不问定义;...x 写「展开」', async () => {
|
||||
const { asked, calleeKind } = kinds({})
|
||||
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
|
||||
expect(text(texts.get(18)?.segments)).toBe('this.definitions = 新建 DefinitionSources · 1 个参数')
|
||||
expect(tableLines(texts.get(18)?.table)).toEqual([
|
||||
'第 1 个 | { rule: 异步 reference => ruleDefinitions(等待 owner.resolveReference(reference)), language: offset => languageDefinitions(document.uri, document.positionAt(offset)) }',
|
||||
])
|
||||
expect(text(texts.get(33)?.segments)).toBe('返回 调用 concatAll · 2 个参数')
|
||||
expect(tableLines(texts.get(33)?.table)).toEqual(['第 1 个 | base', '展开 | extra'])
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, calls, calleeKind)
|
||||
// 实参是模板字符串:照抄
|
||||
expect(propertyLines(rows.get(7) ?? []).slice(1)).toEqual([
|
||||
'初始值: 等待 调用 window.showWarningMessage · 3 个参数',
|
||||
expect.stringMatching(/^ {2}第 1 个 \| 如果 branch 则 `确认删除 worktree [^`]+` 否则 `确认删除 worktree [^`]+`$/),
|
||||
' 第 2 个 | { modal: 真 }',
|
||||
' 第 3 个 | \'删除\'',
|
||||
])
|
||||
expect(propertyLines(rows.get(26) ?? []).slice(1)).toEqual(['初始值: 新建 URL · 1 个参数', expect.stringMatching(/^ {2}第 1 个 \| `[^`]+\/comments`$/)])
|
||||
expect(asked).toEqual([])
|
||||
})
|
||||
|
||||
it('链式调用跨了几行、实参列表各自写在一行里的不排成表;回调主体不在块头部里的调用也不排', async () => {
|
||||
const source = [
|
||||
'const ids = items',
|
||||
' .filter(a => a.ok)',
|
||||
' .map(a => a.id)',
|
||||
'describe(\'x\', () => {',
|
||||
' run()',
|
||||
'})',
|
||||
].join('\n')
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(propertyLines(rows.get(1) ?? []).at(-1)).toBe('值: items .filter(a => a.ok) .map(a => a.id)')
|
||||
const texts = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(texts.get(4)).toEqual(expect.not.objectContaining({ table: expect.anything() }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('中文模板引擎', () => {
|
||||
function testTranslator(table: Partial<ZhTable>): Translator {
|
||||
return createZhTemplateTranslator({
|
||||
@@ -1091,14 +1263,14 @@ describe('中文模板引擎', () => {
|
||||
const source = [
|
||||
'n += a if k in seen else b',
|
||||
'print(a if c else b if d else e)',
|
||||
'print((a # 注释不占序号',
|
||||
' if k == 1 else b))',
|
||||
'return (a # 注释不占序号',
|
||||
' if k == 1 else b)',
|
||||
].join('\n')
|
||||
const map = await translateAll(pythonColored('variable'), 'python', translator, source)
|
||||
expect(texts(map)).toEqual({
|
||||
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
|
||||
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
|
||||
3: 'print((若 k 等于 1 则 a 不然 b))',
|
||||
3: 'return (若 k 等于 1 则 a 不然 b)',
|
||||
})
|
||||
expect(flatSegments(map.get(1)?.segments ?? []).filter(segment => segment.role === 'keyword').map(segment => segment.text))
|
||||
.toEqual(['令', '加上', '若', '则', '不然'])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewTable, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -68,12 +68,23 @@ interface Token {
|
||||
group?: SegmentGroup
|
||||
}
|
||||
|
||||
/** 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。 */
|
||||
/** 跨几行写的调用排成一行、实参摊成表(见 emitCall):只排遇到的第一个,它的表记在 table 里。 */
|
||||
interface CallSlot {
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 按译文顺序收集词:cursor 是上一个词在源码里的结尾,gap 是下一个词前面要不要空一格。
|
||||
* calls 是这一段里跨几行写的调用要不要排成一行、实参摊成表;没有就照常翻。
|
||||
*/
|
||||
interface Collector {
|
||||
document: SourceDocument
|
||||
/** 要翻的那个节点 */
|
||||
root: Node
|
||||
tokens: Token[]
|
||||
cursor: number
|
||||
gap: boolean
|
||||
calls?: CallSlot
|
||||
}
|
||||
|
||||
const CONTEXT_SEPARATOR = ' > '
|
||||
@@ -165,6 +176,9 @@ const KIND_ZH: Record<BlockKind, string> = {
|
||||
}
|
||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||
const ITERATION_ZH: Record<IterationResult, string> = { generator: '生成器', list: '列表', set: '集合', dict: '字典' }
|
||||
/** 被调用的名字定义成这几种块时,调用就是新建对象。 */
|
||||
const CONSTRUCTED = new Set<BlockKind | undefined>(['class', 'exception', 'enum'])
|
||||
const ARGUMENT_COLUMNS = ['参数', '值']
|
||||
const YES_NO: Record<'true' | 'false', string> = { true: '是', false: '否' }
|
||||
|
||||
/** 筛选属性和值的中文叫法;属性表里的访问级别、异步、抽象、仅类型这几行,属性名和值也用这一套。 */
|
||||
@@ -302,9 +316,13 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { style: 'iteration', label: ITERATION_ZH[facts.result], segments }
|
||||
}
|
||||
|
||||
/** 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。 */
|
||||
/**
|
||||
* 遍历整个换成一个框;和外面的调用共用的那对括号照常写在框外,紧贴着框。
|
||||
* 只翻这个遍历(它是调用的实参,排在实参表里)时外面的调用不在译文里,这对括号也不写。
|
||||
*/
|
||||
function emitIteration(out: Collector, node: Node, facts: IterationFacts, span: SourceSpan): void {
|
||||
const shared = facts.sharedParentheses
|
||||
const alone = node.startIndex === out.root.startIndex && node.endIndex === out.root.endIndex
|
||||
const shared = alone ? undefined : facts.sharedParentheses
|
||||
const bracket = (paren: Node): Omit<Token, 'gap' | 'raw'> => ({ text: paren.text, role: out.document.language.tokenRole(paren), translated: false })
|
||||
if (shared)
|
||||
emitSource(out, shared.open, bracket(shared.open))
|
||||
@@ -318,6 +336,60 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
|
||||
function expandedCall(node: Node, span: SourceSpan, calls: CallSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
|
||||
if (calls.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
return undefined
|
||||
const facts = call(node)
|
||||
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
|
||||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs);值照常翻,里面的调用不再排成表。
|
||||
*/
|
||||
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
|
||||
const value = (node: Node): Inline[] => joinTokens(tokensIn(node, span, document))
|
||||
let position = 0
|
||||
const label = (arg: CallArgument): Inline[] => {
|
||||
switch (arg.kind) {
|
||||
case 'keyword':
|
||||
return [segment(arg.name.text, 'variable', offsetOf(arg.name, document.language))]
|
||||
case 'positional':
|
||||
position++
|
||||
return [hint(`第 ${position} 个`)]
|
||||
case 'spread':
|
||||
return [hint('展开')]
|
||||
case 'keywordSpread':
|
||||
return [hint('展开关键字')]
|
||||
}
|
||||
}
|
||||
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(arg => ({ cells: [label(arg), value(arg.value)] })) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
|
||||
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
|
||||
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
|
||||
*/
|
||||
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
|
||||
calls.table = argumentTable(facts, span, out.document)
|
||||
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
|
||||
advance(out, node.startIndex)
|
||||
emit(out, { text: construct ? '新建' : '调用', role: 'storage', translated: true, raw: '' })
|
||||
out.gap = true
|
||||
out.cursor = facts.callee.startIndex
|
||||
const from = out.tokens.length
|
||||
collect(facts.callee, span, out)
|
||||
for (const token of out.tokens.slice(from)) {
|
||||
if (token.offset !== undefined && NAME_ROLES.has(token.role))
|
||||
token.role = token.offset !== facts.name?.startIndex ? 'variable' : construct ? 'type' : 'function'
|
||||
}
|
||||
out.gap = true
|
||||
emit(out, { text: `· ${facts.arguments.length} 个参数`, role: 'hint', translated: true, raw: '' })
|
||||
out.cursor = node.endIndex
|
||||
}
|
||||
|
||||
function collect(node: Node, span: SourceSpan, out: Collector): void {
|
||||
if (node.endIndex <= span.startIndex || node.startIndex >= span.endIndex || node.startIndex === node.endIndex)
|
||||
return
|
||||
@@ -331,6 +403,12 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
emitIteration(out, node, iteration, span)
|
||||
return
|
||||
}
|
||||
const { calls } = out
|
||||
const call = calls && expandedCall(node, span, calls, out.document)
|
||||
if (calls && call) {
|
||||
emitCall(out, calls, node, call, span)
|
||||
return
|
||||
}
|
||||
const parts = templateParts(node, span)
|
||||
if (parts) {
|
||||
advance(out, node.startIndex)
|
||||
@@ -362,9 +440,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument): Token[] {
|
||||
const out: Collector = { document, tokens: [], cursor: span.startIndex, gap: false }
|
||||
/** 按译文顺序收集 node 落在 span 里的词;给了 calls,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, calls?: CallSlot): Token[] {
|
||||
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(calls && { calls }) }
|
||||
collect(node, span, out)
|
||||
return out.tokens
|
||||
}
|
||||
@@ -404,15 +482,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return { markdown: docText(nodes.map(node => commentOf(node, document))) }
|
||||
}
|
||||
|
||||
function translate(block: TextBlock, document: SourceDocument): PreviewText {
|
||||
function translate(block: TextBlock, document: SourceDocument): TextTranslation {
|
||||
if (block.kind === 'comment')
|
||||
return translateComment(block, document)
|
||||
const tokens = tokensIn(block.node, block.header, document)
|
||||
return { text: translateComment(block, document) }
|
||||
const calls: CallSlot = {}
|
||||
const tokens = tokensIn(block.node, block.header, document, calls)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
const last = tokens.at(-1)
|
||||
if (compound && last && !last.translated && HEADER_OPENERS.has(last.raw))
|
||||
tokens.pop()
|
||||
return joinTokens(tokens)
|
||||
return { text: joinTokens(tokens), ...(calls.table && { table: calls.table }) }
|
||||
}
|
||||
|
||||
function expression(node: Node, document: SourceDocument): Inline[] {
|
||||
@@ -428,13 +507,16 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
const { header } = block
|
||||
const { language } = document
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => {
|
||||
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
return joinTokens(tokensIn(node, span, document))
|
||||
}
|
||||
const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document))
|
||||
const phrases: Phrases = {
|
||||
translated: node => within(node, nodeSpan(node)),
|
||||
within,
|
||||
expanded: (node) => {
|
||||
const calls: CallSlot = {}
|
||||
const text = joinTokens(tokensIn(node, clipped(nodeSpan(node)), document, calls))
|
||||
return { text, ...(calls.table && { table: calls.table }) }
|
||||
},
|
||||
entryCount: (literal, size) => {
|
||||
const name = table.mapLiterals[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
@@ -533,6 +615,8 @@ interface Phrases {
|
||||
translated: (node: Node) => Inline[]
|
||||
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
|
||||
within: (node: Node, span: SourceSpan) => Inline[]
|
||||
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
|
||||
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
|
||||
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
|
||||
entryCount: (literal: Node, size: number) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
@@ -760,7 +844,7 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,字典 / 对象字面量见 literalValue);
|
||||
* 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,见 declaredValue);
|
||||
* 说明(有才显示)是整条语句的,排在第一个名称后面。
|
||||
*/
|
||||
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
@@ -770,10 +854,21 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
...(i === 0 ? docRows(block.facts.doc, say) : []),
|
||||
...accessRows(block.facts.access),
|
||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||
{ label: valueLabel, ...(declarator.value ? literalValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,
|
||||
* 实参摊成表(参数 | 值)展开在这一行下面。
|
||||
*/
|
||||
function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
|
||||
if (entries?.length)
|
||||
return literalValue(value, entries, say)
|
||||
const { text, table } = say.expanded(value)
|
||||
return table ? { value: text, ...table } : { value: text }
|
||||
}
|
||||
|
||||
/**
|
||||
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
|
||||
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
|
||||
|
||||
@@ -8,9 +8,10 @@ import { AskThread } from './AskThread'
|
||||
import { BlockTools } from './BlockTools'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { KindIcon } from './kindIcons'
|
||||
import { PropertyGrid } from './PropertyGrid'
|
||||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||||
import { asksOf, historyOf } from './qa'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { translationTableId } from './viewState'
|
||||
|
||||
export interface BlockTreeState {
|
||||
version: number
|
||||
@@ -112,13 +113,21 @@ function BlockQa({ anchor, qa }: { anchor: string, qa: QaTree }): ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
/** 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文,带说明的译文下面接着说明。 */
|
||||
/**
|
||||
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
|
||||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。
|
||||
*/
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
return (
|
||||
<>
|
||||
<TextPreview text={block.translation} />
|
||||
{block.table && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyTable parentId={translationTableId(block.id)} columns={block.table.columns} items={block.table.items} />
|
||||
</div>
|
||||
)}
|
||||
{block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -96,9 +96,9 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
|
||||
|
||||
/**
|
||||
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它。
|
||||
*/
|
||||
function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
||||
export function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
||||
return (
|
||||
<div className="pl-[5px]">
|
||||
<div className="cr-table" role="table" style={{ gridTemplateColumns: tableTemplate(columns.length) }}>
|
||||
|
||||
@@ -95,6 +95,16 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||
})
|
||||
|
||||
it('名字在一行译文下面那张表里(跨几行写的调用的实参):表总是展开的,目标是那块,不用展开什么', () => {
|
||||
const call: BlockView = {
|
||||
...textBlock('0.0', 'return', 1, [word('返回'), word('新建'), name('UserDetailResponse', 10)]),
|
||||
table: { columns: ['参数', '值'], items: [{ cells: [[name('id', 40)], [name('user', 43), { text: '.', role: 'plain' }, name('id', 48)]] }] },
|
||||
}
|
||||
const blocks = [textBlock('0', 'if', 0, [word('如果'), name('user', 3)], [call])]
|
||||
expect(revealPlan(blocks, 9, 48)).toEqual({ id: '0.0', expand: ['0'] })
|
||||
expect(occurrencePlan(blocks, { start: 40, end: 42, kind: 'read' }, () => false)).toEqual({ id: '0.0', expand: ['0'] })
|
||||
})
|
||||
|
||||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||||
const blocks: BlockView[] = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockView, Occurrence, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import type { BlockView, Occurrence, PreviewTable, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import { blockPath } from './filter'
|
||||
import { showsOccurrence } from './occurrences'
|
||||
import { showsOffset } from './segments'
|
||||
@@ -38,19 +38,24 @@ function shows(text: PreviewText, offset: number): boolean {
|
||||
return !('markdown' in text) && showsOffset(text, offset)
|
||||
}
|
||||
|
||||
/** 一行译文下面那张表(总是展开的)里有格子满足 test。 */
|
||||
function tableShows(table: PreviewTable | undefined, test: (cell: PreviewText) => boolean): boolean {
|
||||
return table?.items.some(item => item.cells.some(test)) ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
|
||||
* 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* ③ 块里只是显示着这个名字(一行译文和它下面的表里的名字、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
|
||||
* 不在这块里是 undefined。
|
||||
*/
|
||||
function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
if (covers(block.defines, offset))
|
||||
return { expand: [] }
|
||||
if ('translation' in block)
|
||||
return shows(block.translation, offset) ? { expand: [] } : undefined
|
||||
return shows(block.translation, offset) || tableShows(block.table, cell => shows(cell, offset)) ? { expand: [] } : undefined
|
||||
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
|
||||
for (const { row, id } of rows) {
|
||||
if (covers(row.defines, offset))
|
||||
@@ -120,10 +125,10 @@ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, str
|
||||
return owner === undefined ? plan : { id: owner, expand: blockPath(owner).slice(0, -1) }
|
||||
}
|
||||
|
||||
/** 块里哪儿显示着满足 test 的文字(译文、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
/** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined {
|
||||
if ('translation' in block)
|
||||
return test(block.translation) ? [] : undefined
|
||||
return test(block.translation) || tableShows(block.table, test) ? [] : undefined
|
||||
for (const [index, row] of block.properties.entries()) {
|
||||
if (test(row.value))
|
||||
return []
|
||||
|
||||
@@ -71,6 +71,12 @@ export interface PropertyRow extends Defines {
|
||||
columns?: string[]
|
||||
}
|
||||
|
||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行 */
|
||||
export interface PreviewTable {
|
||||
columns: string[]
|
||||
items: PropertyItem[]
|
||||
}
|
||||
|
||||
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走 */
|
||||
export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
@@ -108,9 +114,9 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 函数、类、枚举、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文;
|
||||
* 一行译文的块带说明时 doc 是说明的 Markdown 原文,排在译文下面
|
||||
* 一行译文下面依次是:跨几行写的调用摊开各个实参的表(table),块的说明(doc,Markdown 原文)
|
||||
*/
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
|
||||
@@ -90,3 +90,8 @@ export function propertyRowId(blockId: string, rowIndex: number): string {
|
||||
export function propertyItemId(parentId: string, itemIndex: number): string {
|
||||
return `${parentId}/${itemIndex}`
|
||||
}
|
||||
|
||||
/** 一行译文下面那张表(跨几行写的调用的实参)的 id:表里各行的 id 是 propertyItemId(它, 行号)。这张表总是展开的。 */
|
||||
export function translationTableId(blockId: string): string {
|
||||
return `${blockId}/t`
|
||||
}
|
||||
Reference in new issue
Block a user