🐛 fix(vscode): React 组件卡片、事件处理函数卡片右上角补上调用树按钮
- 组件卡片、事件处理函数卡片传 calls,被规则认领后照样能看调用树(同自定义钩子卡片) - 编辑器准备不出调用层级(memo / forwardRef 包着的组件)改查引用时,JSX 开始标签也算调用处;闭合标签、成员标签、import、类型位置、当值传递的不算 - 查引用的 lookup 加上取名字前面的文字,判断仍只在 isCallSite 一处
This commit is contained in:
1 parent
5098951c74
commit
57675665a7
8 files changed
+100
-26
No files matched your search
@@ -51,7 +51,8 @@ interface StartCallers {
|
||||
* 这时编辑器认不出起点也照样收前端这一路,起点按名字起点自己写一个。
|
||||
* 给了 references 时查所有用到这个名字的地方:起点按名字起点自己写一个(变量),各处引用(名字自己的定义处、落在 references 这几段里的不算,
|
||||
* 见 [countedSites])按所在的函数、组件并成起点的调用者,从它们往上照常问编辑器。
|
||||
* 都没给、编辑器又认不出起点(数组解构出来的 `setOpen` 这类,见 [referenceCalls])时,起点的调用者同样改查引用、只留调用处,
|
||||
* 都没给、编辑器又认不出起点(数组解构出来的 `setOpen`、`memo(…)` 包着的组件这类,见 [referenceCalls])时,起点的调用者同样改查引用、
|
||||
* 只留调用处(调用它、渲染成 JSX 开始标签的),
|
||||
* 起点同样按名字起点自己写一个。
|
||||
*/
|
||||
export async function loadCallEdges(
|
||||
@@ -118,7 +119,8 @@ export async function loadCallEdges(
|
||||
? countedSites(sites, { uri: document.uri.toString(), line: position.line, character: position.character }, references.map(item => toRange(rangeOf(document, item))))
|
||||
: sites
|
||||
return referenceCalls(counted, {
|
||||
after: site => textAfter(site.uri, site.range),
|
||||
before: async site => (await lineText(site.uri, site.range.line)).slice(0, site.range.character),
|
||||
after: async site => (await lineText(site.uri, site.range.endLine)).slice(site.range.endCharacter),
|
||||
outline: async uri => outlineOf(await run<(DocumentSymbol | SymbolInformation)[]>('vscode.executeDocumentSymbolProvider', Uri.parse(uri)) ?? []),
|
||||
prepare: async (uri, line, character) => {
|
||||
const [item] = await prepare(Uri.parse(uri), new Position(line, character))
|
||||
@@ -187,10 +189,10 @@ function outlineOf(symbols: readonly (DocumentSymbol | SymbolInformation)[]): Ou
|
||||
: { range: toRange(item.location.range), selection: toRange(item.location.range), children: [] })
|
||||
}
|
||||
|
||||
/** 这个范围所在那一行里,范围后面的文字;文件打不开时是空的。 */
|
||||
async function textAfter(uri: string, range: CallRange): Promise<string> {
|
||||
/** 文件里第 `line` 行(从 0 起)的文字,用来看一处引用的名字前后写了什么;文件打不开时是空的。 */
|
||||
async function lineText(uri: string, line: number): Promise<string> {
|
||||
try {
|
||||
return (await workspace.openTextDocument(Uri.parse(uri))).lineAt(range.endLine).text.slice(range.endCharacter)
|
||||
return (await workspace.openTextDocument(Uri.parse(uri))).lineAt(line).text
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
|
||||
@@ -27,8 +27,10 @@ const OUTLINE: OutlineSymbol[] = [
|
||||
|
||||
const KNOWN: Record<string, string> = { '27:16': 'Panel', '41:11': 'handleClick' }
|
||||
|
||||
function lookup(after: Record<string, string>, asked: string[] = []): ReferenceLookup<string> {
|
||||
/** 问编辑器的几样换成假的:after、before 按引用起点(「行:列」)给名字后面、前面的文字,没给的是空的。 */
|
||||
function lookup(after: Record<string, string>, asked: string[] = [], before: Record<string, string> = {}): ReferenceLookup<string> {
|
||||
return {
|
||||
before: async site => before[`${site.range.line}:${site.range.character}`] ?? '',
|
||||
after: async site => after[`${site.range.line}:${site.range.character}`] ?? '',
|
||||
outline: async (uri) => {
|
||||
asked.push(uri)
|
||||
@@ -42,10 +44,30 @@ function lookup(after: Record<string, string>, asked: string[] = []): ReferenceL
|
||||
|
||||
describe('调用关系认不出的名字改查引用', () => {
|
||||
it('只留调用处:名字后面紧跟实参的括号,中间可以隔着空白、?.、泛型实参', () => {
|
||||
expect(['(next)', ' (x)', '?.(x)', '<number>(x)', '] = useState(false)', ' = useCallback(() => {', ', setOpen] =', ')', '.current', '} />'].map(isCallSite))
|
||||
expect(['(next)', ' (x)', '?.(x)', '<number>(x)', '] = useState(false)', ' = useCallback(() => {', ', setOpen] =', ')', '.current', '} />'].map(after => isCallSite(' ', after)))
|
||||
.toEqual([true, true, true, true, false, false, false, false, false, false])
|
||||
})
|
||||
|
||||
it('渲染也算调用处:名字是 JSX 开始标签的标签名(前面紧挨着 <);闭合标签、成员标签、import、类型位置、当值传递的不算', () => {
|
||||
const sites: [before: string, after: string][] = [
|
||||
[' <', ' title="a" />'],
|
||||
[' <', '>'],
|
||||
[' <', ''],
|
||||
[' {open && <', ' />}'],
|
||||
[' return <', '<string> value="x" />'],
|
||||
[' <span>共 {n} 项<', ' /></span>'],
|
||||
[' <DeclCard title="b"></', '>'],
|
||||
[' <', '.Item />'],
|
||||
['import { ', ', ArrowCard } from \'./Widgets\''],
|
||||
['type CardProps = Parameters<typeof ', '>[0]'],
|
||||
[' const ref = useRef<', '>(null)'],
|
||||
['const registry = [', ', ArrowCard]'],
|
||||
[' <section onClick={', '}>'],
|
||||
]
|
||||
expect(sites.map(([before, after]) => isCallSite(before, after)))
|
||||
.toEqual([true, true, true, true, true, true, false, false, false, false, false, false, false])
|
||||
})
|
||||
|
||||
it('每处调用往外找第一个调用关系认得的符号当调用者,同一个调用者的几处并成一项;写在文件顶层的按文件算', async () => {
|
||||
const sites: ReferenceSite[] = [
|
||||
{ uri: URI, range: range(30, 15, 7) },
|
||||
@@ -64,6 +86,22 @@ describe('调用关系认不出的名字改查引用', () => {
|
||||
expect(asked).toEqual([URI, 'file:///main.tsx'])
|
||||
})
|
||||
|
||||
it('渲染成 JSX 开始标签的按所在的组件并进调用者(同调用一样),闭合标签、当值传递的不算', async () => {
|
||||
const sites: ReferenceSite[] = [
|
||||
{ uri: URI, range: range(44, 7, 4) },
|
||||
{ uri: URI, range: range(44, 27, 4) },
|
||||
{ uri: URI, range: range(42, 30, 4) },
|
||||
{ uri: 'file:///main.tsx', range: range(5, 5, 4) },
|
||||
]
|
||||
const before = { '44:7': ' <', '44:27': ' <Card title="a"></', '42:30': ' startTransition(() => [', '5:5': 'root.render(<' }
|
||||
const after = { '44:7': ' title="a">', '44:27': '>', '42:30': '])', '5:5': ' />)' }
|
||||
const calls = await referenceCalls(sites, lookup(after, [], before), () => false, { callsOnly: true })
|
||||
expect(calls).toEqual([
|
||||
{ from: 'Panel', ranges: [range(44, 7, 4)] },
|
||||
{ from: '文件 file:///main.tsx', ranges: [range(5, 5, 4)] },
|
||||
])
|
||||
})
|
||||
|
||||
it('作废了就不要已经收到的', async () => {
|
||||
let done = false
|
||||
const calls = await referenceCalls([{ uri: URI, range: range(34, 4, 7) }], {
|
||||
@@ -78,16 +116,16 @@ describe('调用关系认不出的名字改查引用', () => {
|
||||
})
|
||||
|
||||
describe('查所有引用(React 状态卡片上的「谁用了这个状态」)', () => {
|
||||
it('不只调用处:各处引用都按所在的函数并起来,不问名字后面写了什么', async () => {
|
||||
it('不只调用处:各处引用都按所在的函数并起来,不问名字前后写了什么', async () => {
|
||||
const sites: ReferenceSite[] = [
|
||||
{ uri: URI, range: range(38, 20, 4) },
|
||||
{ uri: URI, range: range(42, 30, 4) },
|
||||
{ uri: URI, range: range(44, 10, 4) },
|
||||
]
|
||||
const after = async (): Promise<string> => {
|
||||
throw new Error('查所有引用不看名字后面')
|
||||
const around = async (): Promise<string> => {
|
||||
throw new Error('查所有引用不看名字前后')
|
||||
}
|
||||
const calls = await referenceCalls(sites, { ...lookup({}), after }, () => false, { callsOnly: false })
|
||||
const calls = await referenceCalls(sites, { ...lookup({}), before: around, after: around }, () => false, { callsOnly: false })
|
||||
expect(calls).toEqual([
|
||||
{ from: 'Panel', ranges: [range(38, 20, 4), range(44, 10, 4)] },
|
||||
{ from: 'handleClick', ranges: [range(42, 30, 4)] },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 按引用找调用者。两种时候用:编辑器的调用关系认不出的名字改查引用(TypeScript 的调用关系只认函数、类、方法这些声明,
|
||||
* 数组解构出来的 `setOpen`(`const [open, setOpen] = useState()`)、`const toggle = useCallback(…)` 的 toggle 都准备不出符号),只留调用处;
|
||||
* 数组解构出来的 `setOpen`(`const [open, setOpen] = useState()`)、`const toggle = useCallback(…)` 的 toggle、`memo(…)` / `forwardRef(…)` 包着的组件
|
||||
* 都准备不出符号),只留调用处(见 [isCallSite]);
|
||||
* 查的就是所有引用(React 状态卡片上「谁用了这个状态」),各处都算,名字自己的定义处、依赖数组里写的那几处除外(见 [countedSites])。
|
||||
* 每处往外找第一个调用关系认得的符号当调用者,再从它往上照常问编辑器。
|
||||
* 问编辑器的动作由外面传入:测试喂假的,宿主去问编辑器命令(见 [查调用关系](vscode/src/codeReader/callTree/query.ts#loadCallEdges))。
|
||||
@@ -23,6 +24,8 @@ export interface OutlineSymbol {
|
||||
|
||||
/** 改查引用时要问编辑器的几样;Caller 是认出来的调用者。 */
|
||||
export interface ReferenceLookup<Caller> {
|
||||
/** 引用那一行里,名字前面的文字 */
|
||||
before: (site: ReferenceSite) => Promise<string>
|
||||
/** 引用那一行里,名字后面的文字 */
|
||||
after: (site: ReferenceSite) => Promise<string>
|
||||
/** 文件的大纲 */
|
||||
@@ -41,9 +44,20 @@ export interface ReferenceCall<Caller> {
|
||||
ranges: CallRange[]
|
||||
}
|
||||
|
||||
/** 名字后面紧跟着实参的括号,这处引用就是在调用它;中间可以隔着空白、可选调用的 `?.`、泛型实参 `<T>`。 */
|
||||
export function isCallSite(after: string): boolean {
|
||||
return /^\s*(?:\?\.\s*)?(?:<[^<>()]*>\s*)?\(/.test(after)
|
||||
/**
|
||||
* 这处引用算不算调用它:调用(名字后面紧跟着实参的括号),或者渲染(名字是 JSX 开始标签的标签名),同编辑器的调用关系一样都算。
|
||||
*
|
||||
* - 调用:名字和括号中间可以隔着空白、可选调用的 `?.`、泛型实参 `<T>`。
|
||||
* - 渲染:名字前面紧挨着 `<`(`<Name …>`、`<Name />`)。闭合标签 `</Name>` 不算;`<` 前面是字母、数字时是泛型实参(`useRef<Name>`)或比较,不算;
|
||||
* 名字后面跟着 `.`、`:` 的(`<Name.Item />`)渲染的是别的组件,也不算。
|
||||
*
|
||||
* @param before 引用那一行里名字前面的文字
|
||||
* @param after 引用那一行里名字后面的文字
|
||||
*/
|
||||
export function isCallSite(before: string, after: string): boolean {
|
||||
const called = /^\s*(?:\?\.\s*)?(?:<[^<>()]*>\s*)?\(/.test(after)
|
||||
const rendered = /(?:^|[^\w$/])<$/.test(before) && !/^[.:]/.test(after)
|
||||
return called || rendered
|
||||
}
|
||||
|
||||
/** 名字在一个文件里的一处位置(名字起点),行和列从 0 起。 */
|
||||
@@ -69,7 +83,7 @@ export function countedSites(sites: readonly ReferenceSite[], declaration: SiteP
|
||||
|
||||
/**
|
||||
* 各处引用按调用者并起来,照第一次出现的顺序:调用者是包着这一处、调用关系认得的最内层符号,都不认得时按文件算。
|
||||
* callsOnly 时只留调用处(名字后面紧跟着实参,见 [isCallSite])。cancelled 变成真就停,已经收到的也不要了。
|
||||
* callsOnly 时只留调用处(调用它、渲染成 JSX 开始标签的,见 [isCallSite])。cancelled 变成真就停,已经收到的也不要了。
|
||||
*/
|
||||
export async function referenceCalls<Caller>(sites: readonly ReferenceSite[], lookup: ReferenceLookup<Caller>, cancelled: () => boolean, { callsOnly }: { callsOnly: boolean }): Promise<ReferenceCall<Caller>[]> {
|
||||
const outlines = new Map<string, Promise<readonly OutlineSymbol[]>>()
|
||||
@@ -77,7 +91,7 @@ export async function referenceCalls<Caller>(sites: readonly ReferenceSite[], lo
|
||||
for (const site of sites) {
|
||||
if (cancelled())
|
||||
return []
|
||||
if (callsOnly && !isCallSite(await lookup.after(site)))
|
||||
if (callsOnly && !isCallSite(await lookup.before(site), await lookup.after(site)))
|
||||
continue
|
||||
const outline = outlines.get(site.uri) ?? lookup.outline(site.uri)
|
||||
outlines.set(site.uri, outline)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 文档发出去之后,在后台数各个调用树的次数:能看调用树的块(函数、类、接口卡片、自定义钩子)落在几棵调用树里,
|
||||
* 文档发出去之后,在后台数各个调用树的次数:能看调用树的块(函数、类、接口卡片、React 的组件、自定义钩子、事件处理函数)落在几棵调用树里,
|
||||
* 卡片属性表里的操作(React 状态卡片解构出的各项)按它的查法数(见 [CallTarget.mode])。问到的都记下(没人调用的也记),数完把次数发给页面。
|
||||
* 页面点开某一个时,问到过有调用者就把树发回去;没问过、或上回没人调用就当场问这一个,问到了补发次数再发树,没问到发一棵空的;
|
||||
* 接口在契约文件里没对上时,发出去的树带着 contractMiss,页面照它说明,不当成没人调用。
|
||||
|
||||
@@ -813,7 +813,7 @@ export interface CallTreeAction {
|
||||
/** 查的那个名字在源码里的范围 */
|
||||
name: SourceRange
|
||||
/**
|
||||
* 怎么查:calls 是查谁调用了它(编辑器的调用关系,认不出这个名字时改查引用、只留调用处),次数是落在几棵调用树里;
|
||||
* 怎么查:calls 是查谁调用了它(编辑器的调用关系,认不出这个名字时改查引用、只留调用处和 JSX 开始标签),次数是落在几棵调用树里;
|
||||
* references 是查所有用到它的地方,按所在的函数、组件分组,次数是用到几处
|
||||
*/
|
||||
mode: 'calls' | 'references'
|
||||
@@ -1237,7 +1237,10 @@ export interface RuleCard {
|
||||
languageKind?: BlockKind
|
||||
/** 卡片的淡底用这个颜色,不填就用种类色([RuleCard.badge] 的颜色);边框仍是种类色。React 钩子返回值的卡片描边是钩子紫,淡底按钩子从哪来分。 */
|
||||
tint?: ThemeColor
|
||||
/** 卡片能看调用树,查卡片自己的名字([RuleCard.defines])被谁调用,按钮在卡片右上角(React 自定义钩子);讲 HTTP 接口的卡片另见 [RuleCard.endpoint]。 */
|
||||
/**
|
||||
* 卡片能看调用树,查卡片自己的名字([RuleCard.defines])被谁调用,按钮在卡片右上角:React 的组件(JSX 开始标签也算调用)、
|
||||
* 自定义钩子、事件处理函数。讲 HTTP 接口的卡片另见 [RuleCard.endpoint]。
|
||||
*/
|
||||
calls?: true
|
||||
/**
|
||||
* 原块里的这几个函数值(实参里的箭头函数、函数表达式)的主体也排成卡片的子块(见 [LanguageAdapter.functionBody]):
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('链式调用卡片发给页面', () => {
|
||||
})
|
||||
|
||||
describe('react 钩子卡片发给页面', () => {
|
||||
it('钩子返回值带着按来源分的淡底;属性表里各项的操作查的名字、不算的范围同别的下标从块第一行的行首算,键原样带着', async () => {
|
||||
it('组件卡片带着能看调用树;钩子返回值带着按来源分的淡底;属性表里各项的操作查的名字、不算的范围同别的下标从块第一行的行首算,键原样带着', async () => {
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const source = [
|
||||
'import { useEffect, useState } from "react";',
|
||||
@@ -106,7 +106,10 @@ describe('react 钩子卡片发给页面', () => {
|
||||
].join('\n')
|
||||
const views = await withBlocks(ts, 'typescriptreact', source, blocks =>
|
||||
toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { languageId: 'typescriptreact', rules: [reactRule], report: () => {} }))
|
||||
const [state, id] = pageBlocks(views, source)[1].children
|
||||
const component = pageBlocks(views, source)[1]
|
||||
expect(component.kind).toBe('react.component')
|
||||
expect(component.callTree).toEqual({})
|
||||
const [state, id] = component.children
|
||||
expect(state.kind).toBe('react.state')
|
||||
expect(state.callTree).toBeUndefined()
|
||||
const base = source.indexOf(' const [n')
|
||||
|
||||
@@ -157,6 +157,12 @@ describe('react 库规则:认领', () => {
|
||||
expect(view.children.map(child => child.slot)).toEqual([undefined, undefined, undefined, undefined])
|
||||
})
|
||||
|
||||
it('组件卡片能看调用树(右上角按钮),查组件名在哪被渲染、被调用', async () => {
|
||||
const view = await card(TITLEBAR, 'DesktopTitlebar')
|
||||
expect(view.callTree).toEqual({})
|
||||
expect(callTargets([view]).find(target => target.key === view.anchor)).toEqual({ key: view.anchor, offset: TITLEBAR.indexOf('DesktopTitlebar ='), mode: 'calls', kind: 'react.component' })
|
||||
})
|
||||
|
||||
it('文件里没有组件时 "use client" 照常成块;组件上面没并进块头部的 JSDoc 讲成说明、不单独成块', async () => {
|
||||
expect((await views('"use client";\nexport const useX = () => 1;')).map(view => view.kind)).toEqual(['statement', 'react.customHook'])
|
||||
const source = ['import { memo } from "react";', '/** 一行标签 */', 'export const Label = memo(({ text }: { text: string }) => <span>{text}</span>);'].join('\n')
|
||||
@@ -469,6 +475,13 @@ describe('react 库规则:luffy 的 QueuedBubble(原型「编辑器全貌」
|
||||
expect(QUEUED.slice(handler.defines?.startIndex, handler.defines?.endIndex)).toBe('handleKeyDown')
|
||||
})
|
||||
|
||||
it('事件处理函数卡片能看调用树(右上角按钮),查它在哪被调用', async () => {
|
||||
const view = await card(QUEUED, 'QueuedEditor')
|
||||
const handler = childAt(view, 186)
|
||||
expect(handler.callTree).toEqual({})
|
||||
expect(callTargets([handler])).toEqual([{ key: handler.anchor, offset: QUEUED.indexOf('handleKeyDown ='), mode: 'calls', kind: 'react.handler' }])
|
||||
})
|
||||
|
||||
it('排队消息组 QueuedMessages:提前 return null 是如果卡片、里面是返回卡片,最后返回的 JSX 照大纲画三元和列表', async () => {
|
||||
const view = await card(QUEUED, 'QueuedMessages')
|
||||
expect(kinds(view)).toEqual(['if 36', 'variable 37', 'react.render 39'])
|
||||
|
||||
@@ -515,8 +515,8 @@ function propsRow(props: PropsFacts, entries: readonly PropEntry[], written: { d
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件卡片:名称 / 说明 / 对不上的参数 / 构造参数 / 异常(说明里的块标签拆开放,见 propDocs)。组件体的语句照常是子块;
|
||||
* 直接返回 JSX 的箭头函数没有子块,它渲染的大纲写在卡片里。
|
||||
* 组件卡片:名称 / 说明 / 对不上的参数 / 构造参数 / 异常(说明里的块标签拆开放,见 [propDocs])。组件体的语句照常是子块;
|
||||
* 直接返回 JSX 的箭头函数没有子块,它渲染的大纲写在卡片里。卡片右上角能看调用树,查这个组件在哪被渲染(JSX 开始标签)、被调用。
|
||||
*/
|
||||
function componentCard(component: ComponentFacts, env: Env): RuleCard {
|
||||
const { context, words } = env
|
||||
@@ -533,7 +533,7 @@ function componentCard(component: ComponentFacts, env: Env): RuleCard {
|
||||
]
|
||||
if (component.body.type !== 'statement_block')
|
||||
rows.push(outlineRow(component.body, env))
|
||||
return card('component', rows, env, { defines: range(component.name) })
|
||||
return card('component', rows, env, { defines: range(component.name), calls: true })
|
||||
}
|
||||
|
||||
// ─── 组件体:名字用在哪 ───────────────────────────────────────────────────────
|
||||
@@ -1149,6 +1149,7 @@ function eventsText(handler: HandlerFacts, analysis: Analysis, env: Env): Inline
|
||||
/**
|
||||
* 事件处理函数:名称 / 说明 / 对不上的参数 / 参数 / 返回(说明里写了 `@return` 才有)/ 异常 / 绑定事件。参数逗号隔开写在一行,
|
||||
* 说明里有参数对上了 `@param` 时排成一张表,多一列说明。函数里的语句照常是这张卡片的子块。
|
||||
* 卡片右上角能看调用树,查它在哪被调用;绑在 JSX 属性上(`onClick={handleClick}`)是把函数当值交出去,不算调用。
|
||||
*/
|
||||
function handlerCard(block: CodeBlock, handler: HandlerFacts, analysis: Analysis, env: Env): RuleCard {
|
||||
const { context, words } = env
|
||||
@@ -1189,7 +1190,7 @@ function handlerCard(block: CodeBlock, handler: HandlerFacts, analysis: Analysis
|
||||
...(doc?.returns ? [{ label: words.labels.returns, value: [], ...returnNote(doc) }] : []),
|
||||
...throwsRows(doc),
|
||||
{ label: words.labels.events, value: eventsText(handler, analysis, env) },
|
||||
], env, { defines: range(handler.name) })
|
||||
], env, { defines: range(handler.name), calls: true })
|
||||
}
|
||||
|
||||
// ─── 渲染输出 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user