🐛 fix(vscode): React 规则在导入了 react 的 .ts / .js 文件上也启用,自定义钩子和钩子调用照常出卡片

This commit is contained in:
cruldra committed 2026-10-16 20:50:12 +11:00
1 parent 2ccec54747
commit 9f736d92e7
23 files changed
+141 -36

No files matched your search

+1 -1
View File
@@ -406,7 +406,7 @@ describe('blockView', () => {
} }
const source = 'class Box:\n pass\n' const source = 'class Box:\n pass\n'
const [view] = await withBlocks(language, 'python', source, blocks => const [view] = await withBlocks(language, 'python', source, blocks =>
toBlockViews(blocks, { source, language, markers: [] }, pythonZhTranslator, { rules: [rule], report: () => {} })) toBlockViews(blocks, { source, language, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [rule], report: () => {} }))
expect(view?.concepts).toEqual([ expect(view?.concepts).toEqual([
{ id: 'class', label: '类', icon: 'box' }, { id: 'class', label: '类', icon: 'box' },
{ id: 'python.abstract', label: '抽象', icon: 'braces' }, { id: 'python.abstract', label: '抽象', icon: 'braces' },
+6 -3
View File
@@ -236,16 +236,19 @@ function embeddedReading({ steps, document, translator }: EmbeddedPart): Embedde
} }
/** /**
* 用哪些库规则(某 languageId 注册的,按注册顺序;这份源码导入了规则关心的库才启用),规则出错时怎么记, * 用哪些库规则(languageId 这种语言注册的,按注册顺序;这份源码导入了规则关心的库才启用,见 [activeRules]),规则出错时怎么记,
* 规则查名字的定义时交出眼下知道的(RuleContext.lookup,见 ruleLookups.ts);没给时总是没查到。 * 规则查名字的定义时交出眼下知道的(RuleContext.lookup,见 ruleLookups.ts);没给时总是没查到。
*/ */
export interface RuleRun { export interface RuleRun {
/** 这份源码的 languageId:规则在有的语言里不看导入就启用([LibraryRule.alwaysIn]) */
languageId: string
rules: readonly LibraryRule[] rules: readonly LibraryRule[]
report: (message: string) => void report: (message: string) => void
lookup?: RuleContext['lookup'] lookup?: RuleContext['lookup']
} }
const NO_RULES: RuleRun = { rules: [], report: () => {} } /** 不用库规则(嵌着的代码):没有规则,languageId 用不上。 */
const NO_RULES: RuleRun = { languageId: '', rules: [], report: () => {} }
/** 源码里一段的结尾:到哪个字符为止(不含),那个字符在第几行。 */ /** 源码里一段的结尾:到哪个字符为止(不含),那个字符在第几行。 */
type SpanEnd = Pick<SourceSpan, 'endIndex' | 'endRow'> type SpanEnd = Pick<SourceSpan, 'endIndex' | 'endRow'>
@@ -366,7 +369,7 @@ export function toBlockViews(
*/ */
function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun, writable: boolean): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => BlockView[] { function layer(layerBlocks: readonly CodeBlock[], layerDocument: SourceDocument, layerTranslator: Translator | undefined, layerRules: RuleRun, writable: boolean): (list: readonly CodeBlock[], prefix: string, scope: string, from?: number) => BlockView[] {
const imports = importsIn(layerBlocks) const imports = importsIn(layerBlocks)
const active = activeRules(layerRules.rules, imports) const active = activeRules(layerRules.rules, imports, layerRules.languageId)
const context: RuleContext = { const context: RuleContext = {
document: layerDocument, document: layerDocument,
imports, imports,
+6 -1
View File
@@ -1343,9 +1343,14 @@ export interface LibraryRule {
/** /**
* 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」「它::」开头(它的子模块;Rust 的路径用 ::)才启用。 * 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」「它::」开头(它的子模块;Rust 的路径用 ::)才启用。
* 宿主按语言插件给的导入判断,规则不用自己读导入语句。空数组是不看导入、languageIds 里这几种语言的源码都启用 * 宿主按语言插件给的导入判断,规则不用自己读导入语句。空数组是不看导入、languageIds 里这几种语言的源码都启用
* (React 组件:新的 JSX 写法不用导入 react,TSX / JSX 文件里就是 React 组件)。 * (Pydantic:子类文件常常只导入父模型)。
*/ */
libraries: string[] libraries: string[]
/**
* languageIds 里不看导入就启用的那几种语言,别的语言照 libraries 看导入;不填就是都照 libraries 看。
* React:新的 JSX 写法不用导入 react,TSX / JSX 文件都启用;.ts / .js 里的 `useXxx` 不一定是钩子(zustand 的 store),导入了 react 才启用。
*/
alwaysIn?: string[]
/** /**
* 看一块:认领就返回替身,不认就返回 undefined。宿主从顶层往下每块都问(被认领的块留下的子块照样问)。 * 看一块:认领就返回替身,不认就返回 undefined。宿主从顶层往下每块都问(被认领的块留下的子块照样问)。
* 几个规则都认同一块时先注册的算;规则抛错、或替身不合上面的约定时,宿主记日志、当它没认,接着问下一个规则。 * 几个规则都认同一块时先注册的算;规则抛错、或替身不合上面的约定时,宿主记日志、当它没认,接着问下一个规则。
@@ -12,7 +12,7 @@ const py = createPythonAdapter(WASM_DIR)
const SNAPSHOT = readFixture('celeryApp.py') const SNAPSHOT = readFixture('celeryApp.py')
function views(source: string): Promise<BlockView[]> { function views(source: string): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [celeryRule], report: () => {} })) return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [celeryRule], report: () => {} }))
} }
/** 记下问了哪边:规则(引用的键)还是语言扩展(下标)。 */ /** 记下问了哪边:规则(引用的键)还是语言扩展(下标)。 */
@@ -80,7 +80,7 @@ export async function documentMessage(
const rules = registry.rulesFor(languageId) const rules = registry.rulesFor(languageId)
const { blocks, colors, displayNames, symbols, imports, kinds, bindings } = await readBlockViews(runtime, adapter, translator, languageId, source, { const { blocks, colors, displayNames, symbols, imports, kinds, bindings } = await readBlockViews(runtime, adapter, translator, languageId, source, {
...(document.fileName && { fileName: document.fileName }), ...(document.fileName && { fileName: document.fileName }),
rules: { rules, report, lookup }, rules: { languageId, rules, report, lookup },
calleeKind, calleeKind,
commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report), commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report),
embedded: (embeddedId) => { embedded: (embeddedId) => {
@@ -97,7 +97,7 @@ describe('react 钩子卡片发给页面', () => {
const ts = createTypeScriptAdapter(WASM_DIR) const ts = createTypeScriptAdapter(WASM_DIR)
const source = ['import { useState } from "react";', 'export function Box() {', ' const [n, setN] = useState(0);', ' return <i>{n}</i>;', '}'].join('\n') const source = ['import { useState } from "react";', 'export function Box() {', ' const [n, setN] = useState(0);', ' return <i>{n}</i>;', '}'].join('\n')
const views = await withBlocks(ts, 'typescriptreact', source, blocks => const views = await withBlocks(ts, 'typescriptreact', source, blocks =>
toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { rules: [reactRule], report: () => {} })) toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { languageId: 'typescriptreact', rules: [reactRule], report: () => {} }))
const hook = pageBlocks(views, source)[1].children[0] const hook = pageBlocks(views, source)[1].children[0]
expect(hook.kind).toBe('react.hook') expect(hook.kind).toBe('react.hook')
expect(hook.tint).toEqual({ dark: '#61dafb', light: '#0a7ea4' }) expect(hook.tint).toEqual({ dark: '#61dafb', light: '#0a7ea4' })
@@ -0,0 +1,42 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useDesktopShellInfo } from "@/hooks/use-desktop-shell-info";
import { useSidebarCollapsed } from "@/lib/sidebar-collapsed";
import { useDesktopShell } from "./store";
import { tauriBridge } from "./tauri-bridge";
/**
* 挂载时经 [tauriBridge] 取一次全量状态与壳留存的活动,之后靠事件同步;不在壳里时什么也不取。
*
* 另在这里注册窗口内快捷键。
*/
export const useShellEvents = () => {
const router = useRouter();
const shell = useDesktopShellInfo();
const { toggle: toggleSidebar } = useSidebarCollapsed();
useEffect(() => {
const s = useDesktopShell.getState();
void tauriBridge.state().then((state) => state && s.applyState(state));
const subs = [
tauriBridge.listen("desktop://new-session", () => router.push("/chat/code")),
];
return () => {
void Promise.all(subs).then((unlisteners) => unlisteners.forEach((unlisten) => unlisten?.()));
};
}, [router]);
// 输入框有焦点时也生效: 这些组合键输入框本来就不吃
useEffect(() => {
if (!shell) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "b") toggleSidebar();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [shell, toggleSidebar]);
};
@@ -24,7 +24,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule]
function views(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<BlockView[]> { function views(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<BlockView[]> {
return withBlocks(adapter, languageId, source, blocks => return withBlocks(adapter, languageId, source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { rules: RULES, report: () => {} })) toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { languageId, rules: RULES, report: () => {} }))
} }
const pyViews = (source: string): Promise<BlockView[]> => views(py, 'python', pythonZhTranslator, source) const pyViews = (source: string): Promise<BlockView[]> => views(py, 'python', pythonZhTranslator, source)
@@ -16,6 +16,7 @@ const sample = readFixture('rustLang.rs')
function views(source: string, fileName?: string): Promise<BlockView[]> { function views(source: string, fileName?: string): Promise<BlockView[]> {
return withBlocks(adapter, 'rust', source, blocks => return withBlocks(adapter, 'rust', source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, { toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, {
languageId: 'rust',
rules: [rustImplRule], rules: [rustImplRule],
report: () => {}, report: () => {},
})) }))
+11 -4
View File
@@ -48,7 +48,7 @@ const throwing: LibraryRule = {
} }
async function views(source: string, rules: LibraryRule[], reports: string[] = []): Promise<BlockView[]> { async function views(source: string, rules: LibraryRule[], reports: string[] = []): Promise<BlockView[]> {
const run: RuleRun = { rules, report: message => reports.push(message) } const run: RuleRun = { languageId: 'python', rules, report: message => reports.push(message) }
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, run)) return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, run))
} }
@@ -56,9 +56,9 @@ function flatten(list: BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)]) return list.flatMap(view => [view, ...flatten(view.children)])
} }
/** 语法树用完即释放,节点要在里面读完。 */ /** 按 Python 解析的源码当成 languageId 这种语言的,规则启不启用;语法树用完即释放,节点要在里面读完。 */
function activates(source: string, rule: LibraryRule): Promise<boolean> { function activates(source: string, rule: LibraryRule, languageId = 'python'): Promise<boolean> {
return withBlocks(py, 'python', source, blocks => activeRules([rule], importsIn(blocks)).length > 0) return withBlocks(py, 'python', source, blocks => activeRules([rule], importsIn(blocks), languageId).length > 0)
} }
describe('库规则:启用', () => { describe('库规则:启用', () => {
@@ -94,6 +94,13 @@ describe('库规则:启用', () => {
expect(await activates('import os', { ...cardRule('any'), libraries: [] })).toBe(true) expect(await activates('import os', { ...cardRule('any'), libraries: [] })).toBe(true)
expect(await activates('x = 1', { ...cardRule('any'), libraries: [] })).toBe(true) expect(await activates('x = 1', { ...cardRule('any'), libraries: [] })).toBe(true)
}) })
it('alwaysIn 里的语言不看导入就启用,别的语言照 libraries 看导入(React:TSX 不用导入 react,.ts 导入了 react 才启用)', async () => {
const rule: LibraryRule = { ...cardRule('any'), languageIds: ['python', 'cython'], alwaysIn: ['cython'] }
expect(await activates('import os', rule, 'cython')).toBe(true)
expect(await activates('import os', rule, 'python')).toBe(false)
expect(await activates('import sqlalchemy', rule, 'python')).toBe(true)
})
}) })
describe('库规则:认领', () => { describe('库规则:认领', () => {
+8 -3
View File
@@ -26,10 +26,15 @@ export function fromLibrary(module: string, library: string): boolean {
return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`) || module.startsWith(`${library}::`) return module === library || module.startsWith(`${library}.`) || module.startsWith(`${library}/`) || module.startsWith(`${library}::`)
} }
/** 这份源码启用的规则,保持注册顺序:导入了规则关心的某个库,或规则不看导入(libraries 是空的)。 */ /**
export function activeRules(rules: readonly LibraryRule[], imports: readonly ImportFacts[]): LibraryRule[] { * languageId 这种语言的一份源码启用的规则,保持注册顺序:导入了规则关心的某个库,或规则不看导入
* (libraries 是空的,或这种语言在 alwaysIn 里)。
*/
export function activeRules(rules: readonly LibraryRule[], imports: readonly ImportFacts[], languageId: string): LibraryRule[] {
const modules = imports.flatMap(facts => facts.sources.map(moduleName)) const modules = imports.flatMap(facts => facts.sources.map(moduleName))
return rules.filter(rule => rule.libraries.length === 0 || rule.libraries.some(library => modules.some(module => fromLibrary(module, library)))) return rules.filter(rule => rule.libraries.length === 0
|| rule.alwaysIn?.includes(languageId)
|| rule.libraries.some(library => modules.some(module => fromLibrary(module, library))))
} }
/** 一块被哪个规则认领、替身是什么。 */ /** 一块被哪个规则认领、替身是什么。 */
+1 -1
View File
@@ -27,7 +27,7 @@ const ROUTES = [
].join('\n') ].join('\n')
function read(source: string): ReturnType<typeof readBlockViews> { function read(source: string): ReturnType<typeof readBlockViews> {
return readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { rules: { rules: [enumRule], report: () => {} } }) return readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { rules: { languageId: 'python', rules: [enumRule], report: () => {} } })
} }
function allProperties(blocks: readonly BlockView[]): string[] { function allProperties(blocks: readonly BlockView[]): string[] {
+1 -1
View File
@@ -13,7 +13,7 @@ const SNAPSHOT = readFixture('celeryApp.py')
async function views(source: string, reports: string[] = []): Promise<BlockView[]> { async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [celeryRule], report: message => reports.push(message) })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [celeryRule], report: message => reports.push(message) }))
} }
function properties(view: BlockView | undefined): PropertyRow[] { function properties(view: BlockView | undefined): PropertyRow[] {
@@ -19,7 +19,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule]
async function views(source: string, reports: string[] = []): Promise<BlockView[]> { async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: RULES, report: message => reports.push(message) })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: RULES, report: message => reports.push(message) }))
} }
function properties(view: BlockView | undefined): PropertyRow[] { function properties(view: BlockView | undefined): PropertyRow[] {
+2 -2
View File
@@ -21,11 +21,11 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule]
async function views(source: string, reports: string[] = []): Promise<BlockView[]> { async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: RULES, report: message => reports.push(message) })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: RULES, report: message => reports.push(message) }))
} }
function tsViews(source: string): Promise<BlockView[]> { function tsViews(source: string): Promise<BlockView[]> {
return withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { rules: RULES, report: () => {} })) return withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { languageId: 'typescript', rules: RULES, report: () => {} }))
} }
function properties(view: BlockView | undefined): PropertyRow[] { function properties(view: BlockView | undefined): PropertyRow[] {
+1 -1
View File
@@ -23,7 +23,7 @@ function known(values: Record<string, string>): RuleContext['lookup'] {
async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = []): Promise<BlockView[]> { async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [fastapiRule], report: message => reports.push(message), lookup })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [fastapiRule], report: message => reports.push(message), lookup }))
} }
function flatten(list: readonly BlockView[]): BlockView[] { function flatten(list: readonly BlockView[]): BlockView[] {
+1 -1
View File
@@ -22,7 +22,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule, pydanticRule
async function views(source: string, reports: string[] = [], lookup?: RuleContext['lookup']): Promise<BlockView[]> { async function views(source: string, reports: string[] = [], lookup?: RuleContext['lookup']): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: RULES, report: message => reports.push(message), lookup })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: RULES, report: message => reports.push(message), lookup }))
} }
async function indexEntries(source: string): Promise<IndexEntry[]> { async function indexEntries(source: string): Promise<IndexEntry[]> {
+45 -6
View File
@@ -1,9 +1,11 @@
import type { Memento } from 'vscode'
import type { BlockView } from '../blockView' import type { BlockView } from '../blockView'
import type { PropertyItem, PropertyRow, RuleContext } from '../contract' import type { PropertyItem, PropertyRow, RuleContext } from '../contract'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView' import { toBlockViews } from '../blockView'
import { diffBlockTrees } from '../diff/blockDiff' import { diffBlockTrees } from '../diff/blockDiff'
import { createTypeScriptAdapter } from '../languages/typescript' import { createTypeScriptAdapter } from '../languages/typescript'
import { CodeReaderRegistry } from '../registry'
import { inlineOf, previewText, propertyLines, propertyRow, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport' import { inlineOf, previewText, propertyLines, propertyRow, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { typescriptZhTranslator } from '../translators/typescriptZh' import { typescriptZhTranslator } from '../translators/typescriptZh'
import { reactRule } from './react' import { reactRule } from './react'
@@ -18,6 +20,7 @@ const SETTINGS = readFixture('desktopSettingsDialog.tsx.txt')
const PLAN = readFixture('planChecklist.tsx.txt') const PLAN = readFixture('planChecklist.tsx.txt')
const QUEUED = readFixture('queuedBubble.tsx.txt') const QUEUED = readFixture('queuedBubble.tsx.txt')
const HOOKS = readFixture('hooksPanel.tsx.txt') const HOOKS = readFixture('hooksPanel.tsx.txt')
const SHELL_EVENTS = readFixture('useShellEvents.ts.txt')
/** 钩子卡片的描边、钩子图标:所有钩子一样的紫 */ /** 钩子卡片的描边、钩子图标:所有钩子一样的紫 */
const HOOK = { dark: '#b180d7', light: '#7f3fa6' } const HOOK = { dark: '#b180d7', light: '#7f3fa6' }
@@ -35,9 +38,13 @@ function known(values: Record<string, string>): RuleContext['lookup'] {
return (_index, node) => values[node.text] === undefined ? undefined : { key: node.text, name: { startIndex: 0, endIndex: 0 }, value: values[node.text] } return (_index, node) => values[node.text] === undefined ? undefined : { key: node.text, name: { startIndex: 0, endIndex: 0 }, value: values[node.text] }
} }
async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = []): Promise<BlockView[]> { /** 只注册了 React 规则的注册表:同宿主一样照文件的 languageId 从这里挑规则。 */
return withBlocks(ts, 'typescriptreact', source, blocks => const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { rules: [reactRule], report: message => reports.push(message), lookup })) registry.registerLibraryRule(reactRule)
async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = [], languageId = 'typescriptreact'): Promise<BlockView[]> {
return withBlocks(ts, languageId, source, blocks =>
toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { languageId, rules: registry.rulesFor(languageId), report: message => reports.push(message), lookup }))
} }
function flatten(list: readonly BlockView[]): BlockView[] { function flatten(list: readonly BlockView[]): BlockView[] {
@@ -83,9 +90,41 @@ function kinds(view: BlockView): string[] {
} }
describe('react 库规则:认领', () => { describe('react 库规则:认领', () => {
it('只管 TSX / JSX,不看导入(新的 JSX 写法不用导入 react)', () => { it('tsx / jsx 不看导入就启用(新的 JSX 写法不用导入 react),.ts / .js 导入了 react 才启用', () => {
expect(reactRule.languageIds).toEqual(['typescriptreact', 'javascriptreact']) expect(reactRule.languageIds).toEqual(['typescriptreact', 'javascriptreact', 'typescript', 'javascript'])
expect(reactRule.libraries).toEqual([]) expect(reactRule.libraries).toEqual(['react'])
expect(reactRule.alwaysIn).toEqual(['typescriptreact', 'javascriptreact'])
})
it('luffy 的 use-shell-events.ts:导入了 react 的 .ts 文件里,自定义钩子出自定义钩子卡片,里面的钩子调用出钩子卡片', async () => {
const blocks = await views(SHELL_EVENTS, undefined, [], 'typescript')
expect(blocks.map(view => `${view.kind} ${view.startLine + 1}`)).toEqual(['statement 1', 'import 3', 'import 4', 'import 6', 'import 7', 'import 9', 'import 10', 'react.customHook 12'])
const hook = blocks[7]
expect(lines(hook)).toEqual([
'名称: useShellEvents ⟦具名导出|⟧',
'说明: 挂载时经 [tauriBridge] 取一次全量状态与壳留存的活动,之后靠事件同步;不在壳里时什么也不取。\n\n另在这里注册窗口内快捷键。',
'参数: 无',
])
expect(kinds(hook)).toEqual(['react.hook 18', 'react.hook 19', 'react.hook 20', 'react.hook 22', 'react.hook 33'])
expect(lines(childAt(hook, 18))).toEqual(['钩子: useRouter', '返回: 1 个', ' router | 路由器,用于页面跳转'])
expect(lines(childAt(hook, 20))).toEqual(['钩子: useSidebarCollapsed', '返回: 1 个', ' toggleSidebar'])
expect(lines(childAt(hook, 22))).toEqual(['钩子: useEffect', '参数: 2 个', ' effect | ⟦函数|· 4 句⟧', ' deps | router 变化时'])
expect(rowLines(childAt(hook, 33), '参数')).toEqual(['参数: 2 个', ' effect | ⟦函数|· 4 句⟧', ' deps | shell、toggleSidebar 变化时'])
})
it('没导入 react 的 .ts 文件不启用:zustand 的 store 叫 useXxx 也照常是常量、函数块;同样的源码写在 .tsx 里照常认', async () => {
const source = [
'import { create } from "zustand";',
'export const useDesktopShell = create<State>((set) => ({ title: "" }));',
'export function useTitle() {',
' const title = useDesktopShell((s) => s.title);',
' return title;',
'}',
].join('\n')
const reports: string[] = []
expect((await views(source, undefined, reports, 'typescript')).map(view => view.kind)).toEqual(['import', 'const', 'function'])
expect((await views(source, undefined, reports, 'typescriptreact')).map(view => view.kind)).toEqual(['import', 'const', 'react.customHook'])
expect(reports).toEqual([])
}) })
it('luffy 的 DesktopTitlebar:五条导入后面是组件卡片,只有名称 / 说明 / 构造参数;组件体照源码顺序是子块,"use client" 和文档注释不单独成块', async () => { it('luffy 的 DesktopTitlebar:五条导入后面是组件卡片,只有名称 / 说明 / 构造参数;组件体照源码顺序是子块,"use client" 和文档注释不单独成块', async () => {
+5 -3
View File
@@ -20,7 +20,8 @@ import { hint, joined, name, plain, range } from './segments'
* `.map` 写「对 … 的每一项」并标 key;属性值是元素、条件或列表时属性名单独成行,值照同一套大纲缩进在下面。 * `.map` 写「对 … 的每一项」并标 key;属性值是元素、条件或列表时属性名单独成行,值照同一套大纲缩进在下面。
* 回调、函数里的语句照常是这些卡片的子块;普通变量、if、注释照语言默认的卡片。表格的格子里只写名字和位置,不放函数体。 * 回调、函数里的语句照常是这些卡片的子块;普通变量、if、注释照语言默认的卡片。表格的格子里只写名字和位置,不放函数体。
* 文件开头的 "use client" 讲成了「客户端组件」、组件上面的 JSDoc 讲成了「说明」,都不再单独成块。 * 文件开头的 "use client" 讲成了「客户端组件」、组件上面的 JSDoc 讲成了「说明」,都不再单独成块。
* 新的 JSX 写法不用导入 react,规则不看导入:TSX / JSX 文件都启用。用词都在词表里,按翻译插件的显示语言挑。 * 新的 JSX 写法不用导入 react,TSX / JSX 文件不看导入就启用;自定义钩子常写在 .ts / .js 文件里,这两种导入了 react 才启用
* (没有 JSX,只认得出自定义钩子和钩子调用)。用词都在词表里,按翻译插件的显示语言挑。
*/ */
const RULE_ID = 'react' const RULE_ID = 'react'
@@ -1193,8 +1194,9 @@ function omitted(block: CodeBlock): boolean {
export const reactRule: LibraryRule = { export const reactRule: LibraryRule = {
id: RULE_ID, id: RULE_ID,
label: 'React 组件', label: 'React 组件',
languageIds: ['typescriptreact', 'javascriptreact'], languageIds: ['typescriptreact', 'javascriptreact', 'typescript', 'javascript'],
libraries: [], libraries: ['react'],
alwaysIn: ['typescriptreact', 'javascriptreact'],
claim: (block, context) => { claim: (block, context) => {
const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH } const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH }
const component = componentOf(block) const component = componentOf(block)
@@ -18,6 +18,7 @@ const rules = [rustImplRule, serdeRule, thiserrorRule, axumRule, sqlxRule, tauri
function views(source: string): Promise<BlockView[]> { function views(source: string): Promise<BlockView[]> {
return withBlocks(adapter, 'rust', source, blocks => return withBlocks(adapter, 'rust', source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, { toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
languageId: 'rust',
rules, rules,
report: (message) => { report: (message) => {
throw new Error(message) throw new Error(message)
@@ -13,7 +13,7 @@ const SNAPSHOT = readFixture('sqlalchemyModels.py')
async function views(source: string, reports: string[] = []): Promise<BlockView[]> { async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
return withBlocks(py, 'python', source, blocks => return withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [sqlalchemyRule], report: message => reports.push(message) })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [sqlalchemyRule], report: message => reports.push(message) }))
} }
function flatten(list: BlockView[]): BlockView[] { function flatten(list: BlockView[]): BlockView[] {
@@ -111,7 +111,7 @@ describe('路由筛选后展开函数体', () => {
parsed, parsed,
{ source, language, markers: [] }, { source, language, markers: [] },
pythonZhTranslator, pythonZhTranslator,
{ rules: [fastapiRule], report: (message) => { throw new Error(message) } }, { languageId: 'python', rules: [fastapiRule], report: (message) => { throw new Error(message) } },
)) ))
const blocks = pageBlocks(views, source) const blocks = pageBlocks(views, source)
const route = blocks.find(block => block.kind === 'fastapi.route')! const route = blocks.find(block => block.kind === 'fastapi.route')!
@@ -502,7 +502,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
async function jump(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, needle: string, rules: readonly LibraryRule[] = []): Promise<{ plan: RevealPlan | undefined, flashed: string }> { async function jump(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, needle: string, rules: readonly LibraryRule[] = []): Promise<{ plan: RevealPlan | undefined, flashed: string }> {
const offset = source.indexOf(needle) const offset = source.indexOf(needle)
expect(offset, needle).toBeGreaterThanOrEqual(0) expect(offset, needle).toBeGreaterThanOrEqual(0)
const views: HostBlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { rules, report: () => {} })) const views: HostBlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { languageId, rules, report: () => {} }))
const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset) const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset)
return { plan, flashed: flashed(views, plan, source) } return { plan, flashed: flashed(views, plan, source) }
} }
@@ -633,7 +633,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
' return self.name', ' return self.name',
].join('\n') ].join('\n')
const views = await withBlocks(py, 'python', source, blocks => const views = await withBlocks(py, 'python', source, blocks =>
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [sqlalchemyRule], report: () => {} })) toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { languageId: 'python', rules: [sqlalchemyRule], report: () => {} }))
const at = (needle: string): { plan: RevealPlan | undefined, flashed: string } => { const at = (needle: string): { plan: RevealPlan | undefined, flashed: string } => {
const offset = source.indexOf(needle) const offset = source.indexOf(needle)
const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset) const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset)