diff --git a/vscode/src/codeReader/blockView.test.ts b/vscode/src/codeReader/blockView.test.ts index 2f6f981..e35c1aa 100644 --- a/vscode/src/codeReader/blockView.test.ts +++ b/vscode/src/codeReader/blockView.test.ts @@ -406,7 +406,7 @@ describe('blockView', () => { } const source = 'class Box:\n pass\n' 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([ { id: 'class', label: '类', icon: 'box' }, { id: 'python.abstract', label: '抽象', icon: 'braces' }, diff --git a/vscode/src/codeReader/blockView.ts b/vscode/src/codeReader/blockView.ts index 40092b4..0532647 100644 --- a/vscode/src/codeReader/blockView.ts +++ b/vscode/src/codeReader/blockView.ts @@ -236,16 +236,19 @@ function embeddedReading({ steps, document, translator }: EmbeddedPart): Embedde } /** - * 用哪些库规则(某 languageId 注册的,按注册顺序;这份源码导入了规则关心的库才启用),规则出错时怎么记, + * 用哪些库规则(languageId 这种语言注册的,按注册顺序;这份源码导入了规则关心的库才启用,见 [activeRules]),规则出错时怎么记, * 规则查名字的定义时交出眼下知道的(RuleContext.lookup,见 ruleLookups.ts);没给时总是没查到。 */ export interface RuleRun { + /** 这份源码的 languageId:规则在有的语言里不看导入就启用([LibraryRule.alwaysIn]) */ + languageId: string rules: readonly LibraryRule[] report: (message: string) => void lookup?: RuleContext['lookup'] } -const NO_RULES: RuleRun = { rules: [], report: () => {} } +/** 不用库规则(嵌着的代码):没有规则,languageId 用不上。 */ +const NO_RULES: RuleRun = { languageId: '', rules: [], report: () => {} } /** 源码里一段的结尾:到哪个字符为止(不含),那个字符在第几行。 */ type SpanEnd = Pick @@ -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[] { const imports = importsIn(layerBlocks) - const active = activeRules(layerRules.rules, imports) + const active = activeRules(layerRules.rules, imports, layerRules.languageId) const context: RuleContext = { document: layerDocument, imports, diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 32cbbe2..2716151 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -1343,9 +1343,14 @@ export interface LibraryRule { /** * 关心的库(模块名):这份源码里有一条导入(ImportFacts.sources,去掉引号)等于它、或以「它.」「它/」「它::」开头(它的子模块;Rust 的路径用 ::)才启用。 * 宿主按语言插件给的导入判断,规则不用自己读导入语句。空数组是不看导入、languageIds 里这几种语言的源码都启用 - * (React 组件:新的 JSX 写法不用导入 react,TSX / JSX 文件里就是 React 组件)。 + * (Pydantic:子类文件常常只导入父模型)。 */ libraries: string[] + /** + * languageIds 里不看导入就启用的那几种语言,别的语言照 libraries 看导入;不填就是都照 libraries 看。 + * React:新的 JSX 写法不用导入 react,TSX / JSX 文件都启用;.ts / .js 里的 `useXxx` 不一定是钩子(zustand 的 store),导入了 react 才启用。 + */ + alwaysIn?: string[] /** * 看一块:认领就返回替身,不认就返回 undefined。宿主从顶层往下每块都问(被认领的块留下的子块照样问)。 * 几个规则都认同一块时先注册的算;规则抛错、或替身不合上面的约定时,宿主记日志、当它没认,接着问下一个规则。 diff --git a/vscode/src/codeReader/editor/definitionSources.test.ts b/vscode/src/codeReader/editor/definitionSources.test.ts index fa8121a..86df345 100644 --- a/vscode/src/codeReader/editor/definitionSources.test.ts +++ b/vscode/src/codeReader/editor/definitionSources.test.ts @@ -12,7 +12,7 @@ const py = createPythonAdapter(WASM_DIR) const SNAPSHOT = readFixture('celeryApp.py') function views(source: string): Promise { - 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: () => {} })) } /** 记下问了哪边:规则(引用的键)还是语言扩展(下标)。 */ diff --git a/vscode/src/codeReader/editor/documentMessage.ts b/vscode/src/codeReader/editor/documentMessage.ts index 7f7be20..2cda601 100644 --- a/vscode/src/codeReader/editor/documentMessage.ts +++ b/vscode/src/codeReader/editor/documentMessage.ts @@ -80,7 +80,7 @@ export async function documentMessage( const rules = registry.rulesFor(languageId) const { blocks, colors, displayNames, symbols, imports, kinds, bindings } = await readBlockViews(runtime, adapter, translator, languageId, source, { ...(document.fileName && { fileName: document.fileName }), - rules: { rules, report, lookup }, + rules: { languageId, rules, report, lookup }, calleeKind, commandRule: commandRuleLookup(registry.commandRulesFor(languageId), report), embedded: (embeddedId) => { diff --git a/vscode/src/codeReader/editor/pageBlocks.test.ts b/vscode/src/codeReader/editor/pageBlocks.test.ts index f99b40f..2c093cc 100644 --- a/vscode/src/codeReader/editor/pageBlocks.test.ts +++ b/vscode/src/codeReader/editor/pageBlocks.test.ts @@ -97,7 +97,7 @@ describe('react 钩子卡片发给页面', () => { const ts = createTypeScriptAdapter(WASM_DIR) const source = ['import { useState } from "react";', 'export function Box() {', ' const [n, setN] = useState(0);', ' return {n};', '}'].join('\n') 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] expect(hook.kind).toBe('react.hook') expect(hook.tint).toEqual({ dark: '#61dafb', light: '#0a7ea4' }) diff --git a/vscode/src/codeReader/fixtures/useShellEvents.ts.txt b/vscode/src/codeReader/fixtures/useShellEvents.ts.txt new file mode 100644 index 0000000..4573505 --- /dev/null +++ b/vscode/src/codeReader/fixtures/useShellEvents.ts.txt @@ -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]); +}; diff --git a/vscode/src/codeReader/languages/exceptions.test.ts b/vscode/src/codeReader/languages/exceptions.test.ts index 7a28f49..abb7a2b 100644 --- a/vscode/src/codeReader/languages/exceptions.test.ts +++ b/vscode/src/codeReader/languages/exceptions.test.ts @@ -24,7 +24,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule] function views(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise { 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 => views(py, 'python', pythonZhTranslator, source) diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 3cbe2b1..cb2f96a 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -16,6 +16,7 @@ const sample = readFixture('rustLang.rs') function views(source: string, fileName?: string): Promise { return withBlocks(adapter, 'rust', source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, { + languageId: 'rust', rules: [rustImplRule], report: () => {}, })) diff --git a/vscode/src/codeReader/libraryRules.test.ts b/vscode/src/codeReader/libraryRules.test.ts index 2e78b1d..3f4913e 100644 --- a/vscode/src/codeReader/libraryRules.test.ts +++ b/vscode/src/codeReader/libraryRules.test.ts @@ -48,7 +48,7 @@ const throwing: LibraryRule = { } async function views(source: string, rules: LibraryRule[], reports: string[] = []): Promise { - 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)) } @@ -56,9 +56,9 @@ function flatten(list: BlockView[]): BlockView[] { return list.flatMap(view => [view, ...flatten(view.children)]) } -/** 语法树用完即释放,节点要在里面读完。 */ -function activates(source: string, rule: LibraryRule): Promise { - return withBlocks(py, 'python', source, blocks => activeRules([rule], importsIn(blocks)).length > 0) +/** 按 Python 解析的源码当成 languageId 这种语言的,规则启不启用;语法树用完即释放,节点要在里面读完。 */ +function activates(source: string, rule: LibraryRule, languageId = 'python'): Promise { + return withBlocks(py, 'python', source, blocks => activeRules([rule], importsIn(blocks), languageId).length > 0) } describe('库规则:启用', () => { @@ -94,6 +94,13 @@ describe('库规则:启用', () => { expect(await activates('import os', { ...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('库规则:认领', () => { diff --git a/vscode/src/codeReader/libraryRules.ts b/vscode/src/codeReader/libraryRules.ts index e0fa575..b712904 100644 --- a/vscode/src/codeReader/libraryRules.ts +++ b/vscode/src/codeReader/libraryRules.ts @@ -26,10 +26,15 @@ export function fromLibrary(module: string, library: string): boolean { 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)) - 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)))) } /** 一块被哪个规则认领、替身是什么。 */ diff --git a/vscode/src/codeReader/nameResolution.test.ts b/vscode/src/codeReader/nameResolution.test.ts index 5c7bc61..3ea9021 100644 --- a/vscode/src/codeReader/nameResolution.test.ts +++ b/vscode/src/codeReader/nameResolution.test.ts @@ -27,7 +27,7 @@ const ROUTES = [ ].join('\n') function read(source: string): ReturnType { - 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[] { diff --git a/vscode/src/codeReader/rules/celery.test.ts b/vscode/src/codeReader/rules/celery.test.ts index ebcc413..ea22b47 100644 --- a/vscode/src/codeReader/rules/celery.test.ts +++ b/vscode/src/codeReader/rules/celery.test.ts @@ -13,7 +13,7 @@ const SNAPSHOT = readFixture('celeryApp.py') async function views(source: string, reports: string[] = []): Promise { 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[] { diff --git a/vscode/src/codeReader/rules/dataclass.test.ts b/vscode/src/codeReader/rules/dataclass.test.ts index e0ad424..2ad265e 100644 --- a/vscode/src/codeReader/rules/dataclass.test.ts +++ b/vscode/src/codeReader/rules/dataclass.test.ts @@ -19,7 +19,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule] async function views(source: string, reports: string[] = []): Promise { 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[] { diff --git a/vscode/src/codeReader/rules/enum.test.ts b/vscode/src/codeReader/rules/enum.test.ts index 2ba9ecb..1d79209 100644 --- a/vscode/src/codeReader/rules/enum.test.ts +++ b/vscode/src/codeReader/rules/enum.test.ts @@ -21,11 +21,11 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule] async function views(source: string, reports: string[] = []): Promise { 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 { - 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[] { diff --git a/vscode/src/codeReader/rules/fastapi.test.ts b/vscode/src/codeReader/rules/fastapi.test.ts index 555110e..bd33a57 100644 --- a/vscode/src/codeReader/rules/fastapi.test.ts +++ b/vscode/src/codeReader/rules/fastapi.test.ts @@ -23,7 +23,7 @@ function known(values: Record): RuleContext['lookup'] { async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = []): Promise { 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[] { diff --git a/vscode/src/codeReader/rules/pydantic.test.ts b/vscode/src/codeReader/rules/pydantic.test.ts index ac4605b..557f89f 100644 --- a/vscode/src/codeReader/rules/pydantic.test.ts +++ b/vscode/src/codeReader/rules/pydantic.test.ts @@ -22,7 +22,7 @@ const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule, pydanticRule async function views(source: string, reports: string[] = [], lookup?: RuleContext['lookup']): Promise { 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 { diff --git a/vscode/src/codeReader/rules/react.test.ts b/vscode/src/codeReader/rules/react.test.ts index cf0aeb0..1261c3d 100644 --- a/vscode/src/codeReader/rules/react.test.ts +++ b/vscode/src/codeReader/rules/react.test.ts @@ -1,9 +1,11 @@ +import type { Memento } from 'vscode' import type { BlockView } from '../blockView' import type { PropertyItem, PropertyRow, RuleContext } from '../contract' import { describe, expect, it } from 'vitest' import { toBlockViews } from '../blockView' import { diffBlockTrees } from '../diff/blockDiff' import { createTypeScriptAdapter } from '../languages/typescript' +import { CodeReaderRegistry } from '../registry' import { inlineOf, previewText, propertyLines, propertyRow, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport' import { typescriptZhTranslator } from '../translators/typescriptZh' import { reactRule } from './react' @@ -18,6 +20,7 @@ const SETTINGS = readFixture('desktopSettingsDialog.tsx.txt') const PLAN = readFixture('planChecklist.tsx.txt') const QUEUED = readFixture('queuedBubble.tsx.txt') const HOOKS = readFixture('hooksPanel.tsx.txt') +const SHELL_EVENTS = readFixture('useShellEvents.ts.txt') /** 钩子卡片的描边、钩子图标:所有钩子一样的紫 */ const HOOK = { dark: '#b180d7', light: '#7f3fa6' } @@ -35,9 +38,13 @@ function known(values: Record): RuleContext['lookup'] { 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 { - return withBlocks(ts, 'typescriptreact', source, blocks => - toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator, { rules: [reactRule], report: message => reports.push(message), lookup })) +/** 只注册了 React 规则的注册表:同宿主一样照文件的 languageId 从这里挑规则。 */ +const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick) +registry.registerLibraryRule(reactRule) + +async function views(source: string, lookup?: RuleContext['lookup'], reports: string[] = [], languageId = 'typescriptreact'): Promise { + 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[] { @@ -83,9 +90,41 @@ function kinds(view: BlockView): string[] { } describe('react 库规则:认领', () => { - it('只管 TSX / JSX,不看导入(新的 JSX 写法不用导入 react)', () => { - expect(reactRule.languageIds).toEqual(['typescriptreact', 'javascriptreact']) - expect(reactRule.libraries).toEqual([]) + it('tsx / jsx 不看导入就启用(新的 JSX 写法不用导入 react),.ts / .js 导入了 react 才启用', () => { + expect(reactRule.languageIds).toEqual(['typescriptreact', 'javascriptreact', 'typescript', 'javascript']) + 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((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 () => { diff --git a/vscode/src/codeReader/rules/react.ts b/vscode/src/codeReader/rules/react.ts index 4a08228..527cd33 100644 --- a/vscode/src/codeReader/rules/react.ts +++ b/vscode/src/codeReader/rules/react.ts @@ -20,7 +20,8 @@ import { hint, joined, name, plain, range } from './segments' * `.map` 写「对 … 的每一项」并标 key;属性值是元素、条件或列表时属性名单独成行,值照同一套大纲缩进在下面。 * 回调、函数里的语句照常是这些卡片的子块;普通变量、if、注释照语言默认的卡片。表格的格子里只写名字和位置,不放函数体。 * 文件开头的 "use client" 讲成了「客户端组件」、组件上面的 JSDoc 讲成了「说明」,都不再单独成块。 - * 新的 JSX 写法不用导入 react,规则不看导入:TSX / JSX 文件都启用。用词都在词表里,按翻译插件的显示语言挑。 + * 新的 JSX 写法不用导入 react,TSX / JSX 文件不看导入就启用;自定义钩子常写在 .ts / .js 文件里,这两种导入了 react 才启用 + * (没有 JSX,只认得出自定义钩子和钩子调用)。用词都在词表里,按翻译插件的显示语言挑。 */ const RULE_ID = 'react' @@ -1193,8 +1194,9 @@ function omitted(block: CodeBlock): boolean { export const reactRule: LibraryRule = { id: RULE_ID, label: 'React 组件', - languageIds: ['typescriptreact', 'javascriptreact'], - libraries: [], + languageIds: ['typescriptreact', 'javascriptreact', 'typescript', 'javascript'], + libraries: ['react'], + alwaysIn: ['typescriptreact', 'javascriptreact'], claim: (block, context) => { const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH } const component = componentOf(block) diff --git a/vscode/src/codeReader/rules/rustLibs.test.ts b/vscode/src/codeReader/rules/rustLibs.test.ts index 532be6c..17976fc 100644 --- a/vscode/src/codeReader/rules/rustLibs.test.ts +++ b/vscode/src/codeReader/rules/rustLibs.test.ts @@ -18,6 +18,7 @@ const rules = [rustImplRule, serdeRule, thiserrorRule, axumRule, sqlxRule, tauri function views(source: string): Promise { return withBlocks(adapter, 'rust', source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, { + languageId: 'rust', rules, report: (message) => { throw new Error(message) diff --git a/vscode/src/codeReader/rules/sqlalchemy.test.ts b/vscode/src/codeReader/rules/sqlalchemy.test.ts index 50611d7..2c349d2 100644 --- a/vscode/src/codeReader/rules/sqlalchemy.test.ts +++ b/vscode/src/codeReader/rules/sqlalchemy.test.ts @@ -13,7 +13,7 @@ const SNAPSHOT = readFixture('sqlalchemyModels.py') async function views(source: string, reports: string[] = []): Promise { 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[] { diff --git a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx index 1f9f88c..fe81678 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx @@ -111,7 +111,7 @@ describe('路由筛选后展开函数体', () => { parsed, { source, language, markers: [] }, 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 route = blocks.find(block => block.kind === 'fastapi.route')! diff --git a/vscode/webview-ui/src/codeReader/reveal.test.ts b/vscode/webview-ui/src/codeReader/reveal.test.ts index b70d4a4..6eb0aab 100644 --- a/vscode/webview-ui/src/codeReader/reveal.test.ts +++ b/vscode/webview-ui/src/codeReader/reveal.test.ts @@ -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 }> { const offset = source.indexOf(needle) 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) return { plan, flashed: flashed(views, plan, source) } } @@ -633,7 +633,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => { ' return self.name', ].join('\n') 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 offset = source.indexOf(needle) const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset)