🐛 fix(vscode): React 规则在导入了 react 的 .ts / .js 文件上也启用,自定义钩子和钩子调用照常出卡片
This commit is contained in:
1 parent
2ccec54747
commit
9f736d92e7
23 files changed
+141
-36
No files matched your search
@@ -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' },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: () => {},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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('库规则:认领', () => {
|
||||||
|
|||||||
@@ -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))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一块被哪个规则认领、替身是什么。 */
|
/** 一块被哪个规则认领、替身是什么。 */
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user