From ba7cc77c21114ea2b76022536e60f7bb3120a5df Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 20:24:33 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=98=85=E8=AF=BB=E9=A2=84=E8=A7=88=20=E2=8C=98=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E8=B7=AF=E7=94=B1=E5=99=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=A1=A8=E9=87=8C=E7=9A=84=E5=87=BD=E6=95=B0=E5=90=8D=E3=80=81?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=A0=91=E7=82=B9=E5=85=A5=E5=8F=A3=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=97=B6=E8=B7=B3=E5=88=B0=E9=82=A3=E5=BC=A0=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=8D=A1=E7=89=87=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=8E=9F?= =?UTF-8?q?=E5=9C=B0=E9=97=AA=E6=8E=A5=E5=8F=A3=E8=A1=A8=E9=87=8C=E9=82=A3?= =?UTF-8?q?=E4=B8=80=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webview-ui/src/codeReader/reveal.test.ts | 36 ++++++++++++++++--- vscode/webview-ui/src/codeReader/reveal.ts | 35 +++++++++++------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/vscode/webview-ui/src/codeReader/reveal.test.ts b/vscode/webview-ui/src/codeReader/reveal.test.ts index 72bf841..694142b 100644 --- a/vscode/webview-ui/src/codeReader/reveal.test.ts +++ b/vscode/webview-ui/src/codeReader/reveal.test.ts @@ -1,5 +1,5 @@ import type { BlockView as HostBlockView } from '../../../src/codeReader/blockView' -import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract' +import type { LanguageAdapter, LibraryRule, Translator } from '../../../src/codeReader/contract' import type { RevealPlan } from './reveal' import type { WrittenBlock } from './testBlocks' import type { BlockKind, Inline, Occurrence, PropertyItem, PropertyRow, Segment } from './types' @@ -8,6 +8,7 @@ import { toBlockViews } from '../../../src/codeReader/blockView' import { pageBlocks } from '../../../src/codeReader/editor/pageBlocks' import { createPythonAdapter } from '../../../src/codeReader/languages/python' import { createTypeScriptAdapter } from '../../../src/codeReader/languages/typescript' +import { fastapiRule } from '../../../src/codeReader/rules/fastapi' import { sqlalchemyRule } from '../../../src/codeReader/rules/sqlalchemy' import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport' import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh' @@ -278,6 +279,18 @@ describe('跳到定义:闪哪一处(defines)', () => { expect(revealIn([parent, fn], 14, 80)).toEqual({ id: '1', expand: [] }) }) + it('前面一块的表格子行里只是显示着这个名字、后面一块自己的名字就是它,两段一样长:停在后面讲这个定义的那块,闪整块', () => { + const router: WrittenBlock = { + ...factBlock('0', 'variable', 0, [ + { label: '名称', value: [name('router', 0)], defines: range(0, 6) }, + { label: '接口', value: [{ text: '1 个', role: 'hint' }], columns: ['方法', '路径', '函数'], items: [{ cells: [[word('POST')], [name('/x', 40)], [{ text: 'create_item', role: 'function', offset: 60 }]] }] }, + ]), + defines: range(0, 6), + } + const route: WrittenBlock = { ...factBlock('1', 'function', 3, [{ label: '名称', value: [{ text: 'create_item', role: 'function', offset: 60 }] }]), defines: range(60, 71) } + expect(revealIn([router, route], 3, 60)).toEqual({ id: '1', expand: [] }) + }) + it('定义是块自己的名字:闪整块,属性行也标了它也一样', () => { expect(revealIn(blocks, 0, 4)).toEqual({ id: '0', expand: [] }) }) @@ -342,11 +355,11 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => { return source.split('\n')[block.startLine].trimStart() } - /** 定义提供者说定义在 source 里 needle 开头那个名字上:预览滚到哪、展开哪些、闪哪一处。 */ - async function jump(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, needle: string): Promise<{ plan: RevealPlan | undefined, flashed: string }> { + /** 定义提供者说定义在 source 里 needle 开头那个名字上:预览滚到哪、展开哪些、闪哪一处。rules 是启用的库规则。 */ + 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)) + const views: HostBlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { rules, report: () => {} })) const plan = revealInSource(views, source, source.slice(0, offset).split('\n').length - 1, offset) return { plan, flashed: flashed(views, plan, source) } } @@ -489,6 +502,21 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => { expect([...collapsedIds(pageBlocks(views, source), {})].sort()).toEqual(['1', '1.0']) }) + it('fastapi:路由器卡片的接口表里也显示着函数名,跳到函数名时停在讲它的接口卡片上,不闪路由器卡片的那一格', async () => { + const source = [ + 'from fastapi import APIRouter', + '', + 'router = APIRouter(prefix="/items")', + '', + '', + '@router.post("/x")', + 'async def create_item(name: str):', + ' """新建一个。"""', + ' return name', + ].join('\n') + expect(await jump(py, 'python', pythonZhTranslator, source, 'create_item(', [fastapiRule])).toEqual({ plan: { id: '2', expand: [] }, flashed: '@router.post("/x")' }) + }) + it('字典 / 对象:定义落在对象的属性名上只闪那一项,嵌套的连外层各层一起展开;值里只是显示着的名字闪整块,展开到它那一层', async () => { const source = [ 'export const routes = {', diff --git a/vscode/webview-ui/src/codeReader/reveal.ts b/vscode/webview-ui/src/codeReader/reveal.ts index f6196f0..d186a96 100644 --- a/vscode/webview-ui/src/codeReader/reveal.ts +++ b/vscode/webview-ui/src/codeReader/reveal.ts @@ -12,11 +12,15 @@ export interface RevealPlan { expand: string[] } -/** 目标在一块里的哪儿:row、expand 同 RevealPlan,expand 不含祖先块。span 是盖住定义下标的那段源码有多长。 */ +/** + * 目标在一块里的哪儿:`row`、`expand` 同 [RevealPlan],`expand` 不含祖先块。`span` 是盖住定义下标的那段源码有多长。 + * `defining` 说这一处是讲这个定义的([locate] 的 ①②),不是只显示着这个名字(③)。 + */ interface Spot { row?: string expand: string[] span: number + defining: boolean } interface Placed { @@ -98,7 +102,7 @@ function* textItems(blockId: string, items: PropertyItem[] | undefined, table: P function definedItem(items: Iterable, offset: number): Spot | undefined { for (const { item, id, expand } of items) { if (item.defines && covers(item.defines, offset)) - return { row: id, expand, span: width(item.defines) } + return { row: id, expand, span: width(item.defines), defining: true } } return undefined } @@ -109,22 +113,22 @@ function definedItem(items: Iterable, offset: number): Spot | undefi * ② 属性表、一行译文下面的子行里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它, * 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开; * ③ 块里只是显示着这个名字(一段源码范围盖住 offset:一行译文和它下面拆开的各项、表里的名字,没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。 - * span 是盖住 offset 的那段有多长。不在这块里是 undefined。 + * `span` 是盖住 `offset` 的那段有多长,`defining` 标出是 ①② 还是 ③,[revealPlan] 拿这两样在几块之间挑(见 [closer])。不在这块里是 `undefined`。 */ function locate(block: BlockView, offset: number): Spot | undefined { if (block.defines && covers(block.defines, offset)) - return { expand: [], span: width(block.defines) } + return { expand: [], span: width(block.defines), defining: true } if ('translation' in block) { const defined = definedItem(textItems(block.id, block.items, block.table), offset) if (defined) return defined const span = textSpan(block.translation, offset) - return span === undefined ? shownItem(textItems(block.id, block.items, block.table), offset) : { expand: [], span } + return span === undefined ? shownItem(textItems(block.id, block.items, block.table), offset) : { expand: [], span, defining: false } } const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) })) for (const { row, id } of rows) { if (row.defines && covers(row.defines, offset)) - return { row: id, expand: [], span: width(row.defines) } + return { row: id, expand: [], span: width(row.defines), defining: true } const defined = definedItem(rowItems(row, id), offset) if (defined) return defined @@ -132,7 +136,7 @@ function locate(block: BlockView, offset: number): Spot | undefined { for (const { row, id } of rows) { const value = textSpan(row.value, offset) if (value !== undefined) - return { expand: [], span: value } + return { expand: [], span: value, defining: false } const shown = shownItem(rowItems(row, id), offset) if (shown) return shown @@ -146,7 +150,7 @@ function shownItem(items: Iterable, offset: number): Spot | undefine for (const cell of item.cells) { const span = textSpan(cell, offset) if (span !== undefined) - return { expand, span } + return { expand, span, defining: false } } } return undefined @@ -161,15 +165,22 @@ function showingItem(items: Iterable, test: (cell: PreviewText) => b return undefined } +/** 在几块里挑目标时,`spot` 是否比眼下选中的 `best` 更贴近这处定义:盖住下标的那段更短的更贴近,一样短时讲这个定义的胜过只是显示着这个名字的。 */ +function closer(spot: Spot, best: Spot): boolean { + return spot.span !== best.span ? spot.span < best.span : spot.defining && !best.defining +} + /** - * 跳到定义:目标见 locate。几块的源码范围都盖住这个下标时,留盖住它的那段最短的块—— + * 跳到定义:目标见 [locate]。几块的源码范围都盖住这个下标时,留盖住它的那段最短的块—— * 父块的大范围盖住后面那个函数的名字时,停在函数上,不闪父块。 + * 一样短时,讲这个定义的块胜过只是显示着这个名字的块,再一样就取先遇到的—— + * 路由器卡片的接口表里显示着的函数名和下面接口卡片自己的名字一样长,停在接口卡片上,不闪接口表里那一格。 * 页面上找不到这处定义,就取包住 line 的块(块在文件里的首尾行之间);几块都包住时取行数最少的,一样少时取先遇到的外层。 * 没有任何块包住这一行(它在所有块之前,或落在块与块的空档)时取第一块。没有块时没有目标。 * 目标的祖先块都要展开。line、definition 是文件里的,各块里的位置按 placement 换算。 */ export function revealPlan(blocks: BlockView[], placement: Placement, line: number, definition: number): RevealPlan | undefined { - let best: { plan: RevealPlan, span: number } | undefined + let best: { plan: RevealPlan, spot: Spot } | undefined let byLine: Placed | undefined let byLineSpan = Number.POSITIVE_INFINITY for (const item of placed(blocks)) { @@ -177,8 +188,8 @@ export function revealPlan(blocks: BlockView[], placement: Placement, line: numb if (!place) continue const spot = locate(item.block, definition - place.offset) - if (spot && (best === undefined || spot.span < best.span)) - best = { span: spot.span, plan: { id: item.block.id, ...(spot.row !== undefined && { row: spot.row }), expand: [...item.ancestors, ...spot.expand] } } + if (spot && (best === undefined || closer(spot, best.spot))) + best = { spot, plan: { id: item.block.id, ...(spot.row !== undefined && { row: spot.row }), expand: [...item.ancestors, ...spot.expand] } } if (place.line <= line && line <= place.endLine) { const span = place.endLine - place.line if (span < byLineSpan) {