🐛 fix(vscode): 代码阅读注释里引用的语言内置名不再标成找不到:代码里写到的(raise ValueError)问那处的定义、点了跳到语言自带的定义,没写到的当普通文字不能点;内置名单由语言插件给(Python 补全 dir(builtins),TypeScript 是 ECMAScript 标准全局对象)
This commit is contained in:
1 parent
49a3854390
commit
e3c8c1b118
14 files changed
+544
-98
No files matched your search
@@ -775,6 +775,11 @@ export interface LanguageAdapter {
|
||||
* 只报认得准的:拿不准的(模式匹配捕获的、跳过不看的声明里的、同一个名字既导入又赋值的……)不报,宿主照旧逐处问。不填就逐处问。
|
||||
*/
|
||||
bindings?: (root: Node, source: string, languageId: string) => NameBinding[]
|
||||
/**
|
||||
* 这门语言自带、不用导入就能用的名字(Python 的 `ValueError`、`str`、`len`、`None`,TypeScript 的 `Error`、`Promise`、`JSON`),
|
||||
* 注释里引用了它、预览却找不到定义时靠它认出来,当普通文字显示,不标成找不到;没有的语言不填。
|
||||
*/
|
||||
builtins?: ReadonlySet<string>
|
||||
/** 这份源码里嵌着的别的语言的代码(见 EmbeddedCode),块里的(TextBlock.embedded)、属性表里的(HealthFacts.command)都算;没有的语言不填。 */
|
||||
embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[]
|
||||
/** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块(见 StepBlocks);不填就按 toBlocks 分、不算一步失败就停。 */
|
||||
|
||||
@@ -211,9 +211,11 @@ export interface StickyScrollSettings {
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
|
||||
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
|
||||
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条)
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用,
|
||||
* builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`,页面当普通文字);两样都原样带回问的那几条
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
|
||||
* block 同 openRepoLink
|
||||
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block 同 openRepoLink
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
* callTreeCounts:version 这份文档里,每个能看调用树的块(BlockView.callTree)落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
@@ -242,8 +244,9 @@ export type ExtensionToWebview
|
||||
| { type: 'rewriteEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
|
||||
| { type: 'diff', diff: DocumentDiff | null }
|
||||
| { type: 'openedFrom', from: OpenedFrom }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[] }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
|
||||
| ({ type: 'repoLinkBuiltin' } & SymbolReference)
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
|
||||
@@ -567,11 +567,26 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
|
||||
await page.send({ type: 'ready' })
|
||||
|
||||
await page.send({ type: 'checkRepoLinks', id: 1, paths: [], references: [{ symbol: 'get_agent_admitted', block: METHOD }, { symbol: 'get_agent_admitted', block: HELPER }] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'get_agent_admitted', block: HELPER }] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'get_agent_admitted', block: HELPER }], builtin: [] })
|
||||
|
||||
await page.send({ type: 'openRepoLink', symbol: 'get_agent_admitted', block: HELPER })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', symbol: 'get_agent_admitted', block: HELPER })
|
||||
})
|
||||
|
||||
it('语言自带、代码里没写到的名字:检查时报成 builtin,点了报 repoLinkBuiltin,不标坏', async () => {
|
||||
vi.mocked(documentSymbolNodes).mockResolvedValue([])
|
||||
const { provider, document } = opening(undefined, undefined, () => ({ builtins: new Set(['Error']) }))
|
||||
const page = tab()
|
||||
provider.resolveCustomTextEditor(document as never, page.panel)
|
||||
await page.send({ type: 'ready' })
|
||||
|
||||
await page.send({ type: 'checkRepoLinks', id: 1, paths: [], references: [{ symbol: 'Error', block: HELPER }, { symbol: 'Missing', block: HELPER }] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'Missing', block: HELPER }], builtin: [{ symbol: 'Error', block: HELPER }] })
|
||||
|
||||
await page.send({ type: 'openRepoLink', symbol: 'Error', block: HELPER })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBuiltin', symbol: 'Error', block: HELPER })
|
||||
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'repoLinkBroken' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('点了带几行的仓库链接', () => {
|
||||
@@ -624,7 +639,7 @@ describe('点了带几行的仓库链接', () => {
|
||||
it('问哪些路径没有时,带几行的链接按文件问', async () => {
|
||||
const { page } = await linked()
|
||||
await page.send({ type: 'checkRepoLinks', id: 3, paths: ['web/a.ts', 'web/gone.ts'], references: [] })
|
||||
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 3, missing: ['web/gone.ts'], unresolved: [] }])
|
||||
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 3, missing: ['web/gone.ts'], unresolved: [], builtin: [] }])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, WebviewPanel } from 'vscode'
|
||||
import type { ClaudeImage } from '../../cc/spawnClaude'
|
||||
import type { CommentEdit, CommentJob, WriteResult } from '../commentRewrite'
|
||||
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference } from '../contract'
|
||||
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, SourceRange } from '../contract'
|
||||
import type { OpenedFrom, PreviewDiffBases } from '../diff/diffBase'
|
||||
import type { OldDocument, SentDocument } from '../diff/previewDiff'
|
||||
import type { WorkspaceEntries } from '../entries/workspaceEntries'
|
||||
@@ -10,7 +10,7 @@ import type { CodeQa, TopRequest } from '../qa/codeQa'
|
||||
import type { CodeReaderRegistry } from '../registry'
|
||||
import type { LookupSources } from '../ruleLookups'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { ImportedName, LinkTarget, ReferenceTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { ThemeColors } from '../theme/codeColors'
|
||||
import type { IndexFind } from '../workspaceIndex'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
@@ -37,7 +37,7 @@ import { NOT_REWRITABLE } from '../qa/codeQa'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { referenceScope, resolveReference } from '../symbolLinks'
|
||||
import { outsideUses, referenceScope, resolveReference } from '../symbolLinks'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { definitionSiteNames, namesOnCard, pagePeekBlock, peekBlock } from './definitionPeek'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
@@ -245,6 +245,8 @@ class Preview {
|
||||
private symbols: { version: number, list: LocalSymbol[] } | undefined
|
||||
/** 最近一次发出去的文档里各条导入引进来的名字:注释里不带路径的引用按它找导入的定义 */
|
||||
private imports: ImportedName[] = []
|
||||
/** 最近一次发出去的文档里,代码用到、本文件又没定义的名字各在哪几处(见 [outsideUses]):注释里的引用按它找语言自带的名字的定义 */
|
||||
private uses = new Map<string, SourceRange[]>()
|
||||
/** 这份文档各版本显示成中文名的名字 */
|
||||
private readonly names: DocumentNames
|
||||
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
|
||||
@@ -443,6 +445,7 @@ class Preview {
|
||||
this.sent = message.error === undefined ? { version: message.version, blocks: message.blocks, source } : undefined
|
||||
this.symbols = { version, list: symbols }
|
||||
this.imports = imports
|
||||
this.uses = outsideUses(bindings, source)
|
||||
const reveal = this.pendingReveal
|
||||
this.pendingReveal = undefined
|
||||
this.shown = version
|
||||
@@ -852,12 +855,12 @@ class Preview {
|
||||
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
|
||||
}
|
||||
|
||||
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到没有。 */
|
||||
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到、是语言自带的名字时没有。 */
|
||||
private async peekLink(id: number, rel: string | undefined, symbol: string, block: string | undefined): Promise<void> {
|
||||
let peek: DefinitionPeek | undefined
|
||||
try {
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
peek = target && await this.peekAt(Uri.parse(target.uri), target.selection.startIndex)
|
||||
peek = target && target !== 'builtin' ? await this.peekAt(Uri.parse(target.uri), target.selection.startIndex) : undefined
|
||||
}
|
||||
catch (err) {
|
||||
const link = rel === undefined ? `[${symbol}]` : `${rel}#${symbol}`
|
||||
@@ -927,7 +930,7 @@ class Preview {
|
||||
}
|
||||
}
|
||||
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到。 */
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到、哪些是语言自带的名字(页面当普通文字)。 */
|
||||
private async checkRepoLinks(id: number, paths: string[], references: SymbolReference[]): Promise<void> {
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const asked = new Map<string, Promise<SymbolNode[]>>()
|
||||
@@ -936,15 +939,21 @@ class Preview {
|
||||
asked.set(uri, known)
|
||||
return known
|
||||
}
|
||||
const [missing, unresolved] = await Promise.all([
|
||||
const [missing, resolved] = await Promise.all([
|
||||
Promise.all(paths.map(async (rel) => {
|
||||
if (root === undefined || await resolveRepoEntry(root, rel) === undefined)
|
||||
return rel
|
||||
return undefined
|
||||
})),
|
||||
Promise.all(references.map(async reference => await this.resolveSymbolLink(reference, symbolsOf) ? undefined : reference)),
|
||||
Promise.all(references.map(async reference => ({ reference, target: await this.resolveSymbolLink(reference, symbolsOf) }))),
|
||||
])
|
||||
this.post({ type: 'repoLinksChecked', id, missing: missing.filter(rel => rel !== undefined), unresolved: unresolved.filter(reference => reference !== undefined) })
|
||||
this.post({
|
||||
type: 'repoLinksChecked',
|
||||
id,
|
||||
missing: missing.filter(rel => rel !== undefined),
|
||||
unresolved: resolved.filter(({ target }) => target === undefined).map(({ reference }) => reference),
|
||||
builtin: resolved.filter(({ target }) => target === 'builtin').map(({ reference }) => reference),
|
||||
})
|
||||
}
|
||||
|
||||
/** 仓库根放进这个预览能读的本地目录,图片地址才能加载。真实位置和 git 给的路径不一样时两个都放。 */
|
||||
@@ -988,13 +997,17 @@ class Preview {
|
||||
/**
|
||||
* 打开注释、问 AI 的回答里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
|
||||
* 带了符号就按文档符号的名字跳过去,带了几行(`lines`)就跳到这几行(见 [linesSpot]);文件或符号没有就不打开。
|
||||
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink])。
|
||||
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
|
||||
*/
|
||||
private async openRepoLink(rel: string | undefined, symbol?: string, block?: string, lines?: LineRange): Promise<void> {
|
||||
const link = rel ?? `[${symbol}]`
|
||||
try {
|
||||
if (symbol !== undefined) {
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
if (target === 'builtin') {
|
||||
this.post({ type: 'repoLinkBuiltin', symbol, ...(block !== undefined && { block }) })
|
||||
return
|
||||
}
|
||||
if (!target) {
|
||||
this.markRepoLinkBroken(rel, symbol, block)
|
||||
return
|
||||
@@ -1031,9 +1044,10 @@ class Preview {
|
||||
|
||||
/**
|
||||
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件、从它写在的那块(block)找(resolveSymbolLink);
|
||||
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有。点链接、悬停链接看定义卡片都按它找。
|
||||
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有;不带路径的引用是语言自带的名字时是 `'builtin'`。
|
||||
* 点链接、悬停链接看定义卡片都按它找。
|
||||
*/
|
||||
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<LinkTarget | undefined> {
|
||||
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<ReferenceTarget> {
|
||||
if (rel === undefined)
|
||||
return this.resolveSymbolLink({ symbol, block })
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
@@ -1048,13 +1062,15 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(block,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
|
||||
* 再是导入进来的(见 symbolLinks.ts)。检查坏链接、点击、悬停看定义卡片都走这里。
|
||||
* 文档符号同仓库链接找符号那一套(symbols,一批引用一起问时共用一份),导入的名字问语言扩展它的定义(同跳到定义),定义几处取第一处。
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(`block`,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
|
||||
* 再是导入进来的、代码里用到的语言自带的名字,语言自带又问不到定义的是 `'builtin'`(见 [resolveReference])。检查坏链接、点击、悬停看定义卡片都走这里。
|
||||
* 文档符号同仓库链接找符号那一套(`symbols`,一批引用一起问时共用一份),导入的、代码里用到的名字问语言扩展它的定义(同跳到定义),定义几处取第一处;
|
||||
* 哪些名字是语言自带的问这门语言的语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))。
|
||||
*/
|
||||
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<LinkTarget | undefined> {
|
||||
const { document, sent } = this
|
||||
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<ReferenceTarget> {
|
||||
const { document, sent, uses } = this
|
||||
const at = block === undefined || !sent ? undefined : referenceScope(sent.blocks, block)
|
||||
const builtins = this.registry.languageFor(document.languageId)?.builtins
|
||||
try {
|
||||
return await resolveReference(symbol, at, this.imports, {
|
||||
uri: document.uri.toString(),
|
||||
@@ -1066,6 +1082,8 @@ class Preview {
|
||||
const doc = await workspace.openTextDocument(found.uri)
|
||||
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
|
||||
},
|
||||
uses: name => uses.get(name) ?? [],
|
||||
builtin: name => builtins?.has(name) ?? false,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -541,6 +541,96 @@ const BUILTIN_EXCEPTIONS = new Set([
|
||||
'UnicodeWarning',
|
||||
'UserWarning',
|
||||
])
|
||||
/** Python 自带的其余名字(`dir(builtins)` 里公开的):内置类型、内置函数、常量。 */
|
||||
const BUILTIN_OTHERS = [
|
||||
// 内置类型
|
||||
'bool',
|
||||
'bytearray',
|
||||
'bytes',
|
||||
'classmethod',
|
||||
'complex',
|
||||
'dict',
|
||||
'enumerate',
|
||||
'filter',
|
||||
'float',
|
||||
'frozenset',
|
||||
'int',
|
||||
'list',
|
||||
'map',
|
||||
'memoryview',
|
||||
'object',
|
||||
'property',
|
||||
'range',
|
||||
'reversed',
|
||||
'set',
|
||||
'slice',
|
||||
'staticmethod',
|
||||
'str',
|
||||
'super',
|
||||
'tuple',
|
||||
'type',
|
||||
'zip',
|
||||
// 内置函数,含启动时 site 加进来的 help、exit、quit、copyright、credits、license
|
||||
'abs',
|
||||
'aiter',
|
||||
'all',
|
||||
'anext',
|
||||
'any',
|
||||
'ascii',
|
||||
'bin',
|
||||
'breakpoint',
|
||||
'callable',
|
||||
'chr',
|
||||
'compile',
|
||||
'copyright',
|
||||
'credits',
|
||||
'delattr',
|
||||
'dir',
|
||||
'divmod',
|
||||
'eval',
|
||||
'exec',
|
||||
'exit',
|
||||
'format',
|
||||
'getattr',
|
||||
'globals',
|
||||
'hasattr',
|
||||
'hash',
|
||||
'help',
|
||||
'hex',
|
||||
'id',
|
||||
'input',
|
||||
'isinstance',
|
||||
'issubclass',
|
||||
'iter',
|
||||
'len',
|
||||
'license',
|
||||
'locals',
|
||||
'max',
|
||||
'min',
|
||||
'next',
|
||||
'oct',
|
||||
'open',
|
||||
'ord',
|
||||
'pow',
|
||||
'print',
|
||||
'quit',
|
||||
'repr',
|
||||
'round',
|
||||
'setattr',
|
||||
'sorted',
|
||||
'sum',
|
||||
'vars',
|
||||
// 常量
|
||||
'None',
|
||||
'True',
|
||||
'False',
|
||||
'Ellipsis',
|
||||
'NotImplemented',
|
||||
]
|
||||
|
||||
/** Python 不用导入就能用的名字([LanguageAdapter.builtins]):自带的异常类加上 [BUILTIN_OTHERS]。 */
|
||||
const PYTHON_BUILTINS: ReadonlySet<string> = new Set([...BUILTIN_EXCEPTIONS, ...BUILTIN_OTHERS])
|
||||
|
||||
/** PEP 8 的命名习惯:异常类名以 Error 结尾(别的文件里的 `AppException`、`*Warning` 也常见)。 */
|
||||
const EXCEPTION_NAME = /(?:Error|Exception|Warning)$/
|
||||
|
||||
@@ -672,6 +762,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
call: pythonCall,
|
||||
collection: pythonCollection,
|
||||
bindings: root => pythonBindings(root),
|
||||
builtins: PYTHON_BUILTINS,
|
||||
displayNames: pythonNameReader,
|
||||
}
|
||||
}
|
||||
@@ -530,6 +530,76 @@ const TYPESCRIPT_EXCEPTIONS: ExceptionRules = {
|
||||
constructorParameters: block => block.kind === 'function' && block.facts.name?.text === 'constructor' ? block.facts.parameters : undefined,
|
||||
}
|
||||
|
||||
// ─── 自带的名字 ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** ECMAScript 标准的全局对象([LanguageAdapter.builtins]),国际化的 `Intl` 也算;浏览器、Node 另加的不算。 */
|
||||
const TYPESCRIPT_BUILTINS: ReadonlySet<string> = new Set([
|
||||
// 值
|
||||
'globalThis',
|
||||
'Infinity',
|
||||
'NaN',
|
||||
'undefined',
|
||||
// 函数
|
||||
'eval',
|
||||
'isFinite',
|
||||
'isNaN',
|
||||
'parseFloat',
|
||||
'parseInt',
|
||||
'decodeURI',
|
||||
'decodeURIComponent',
|
||||
'encodeURI',
|
||||
'encodeURIComponent',
|
||||
// 构造函数
|
||||
'AggregateError',
|
||||
'Array',
|
||||
'ArrayBuffer',
|
||||
'BigInt',
|
||||
'BigInt64Array',
|
||||
'BigUint64Array',
|
||||
'Boolean',
|
||||
'DataView',
|
||||
'Date',
|
||||
'Error',
|
||||
'EvalError',
|
||||
'FinalizationRegistry',
|
||||
'Float16Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Function',
|
||||
'Int8Array',
|
||||
'Int16Array',
|
||||
'Int32Array',
|
||||
'Iterator',
|
||||
'Map',
|
||||
'Number',
|
||||
'Object',
|
||||
'Promise',
|
||||
'Proxy',
|
||||
'RangeError',
|
||||
'ReferenceError',
|
||||
'RegExp',
|
||||
'Set',
|
||||
'SharedArrayBuffer',
|
||||
'String',
|
||||
'Symbol',
|
||||
'SyntaxError',
|
||||
'TypeError',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Uint16Array',
|
||||
'Uint32Array',
|
||||
'URIError',
|
||||
'WeakMap',
|
||||
'WeakRef',
|
||||
'WeakSet',
|
||||
// 命名空间对象
|
||||
'Atomics',
|
||||
'Intl',
|
||||
'JSON',
|
||||
'Math',
|
||||
'Reflect',
|
||||
])
|
||||
|
||||
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 流程关键字;其余字母关键字都算声明关键字(storage)。 */
|
||||
@@ -664,6 +734,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
call: typescriptCall,
|
||||
collection: typescriptCollection,
|
||||
bindings: root => typescriptBindings(root),
|
||||
builtins: TYPESCRIPT_BUILTINS,
|
||||
displayNames: typescriptNameReader,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { LanguageAdapter, SourceRange } from './contract'
|
||||
import type { ReferenceSources, SymbolNode } from './symbolLinks'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { importsIn } from './libraryRules'
|
||||
import { importedNames, referenceScope, resolveReference, splitReference } from './symbolLinks'
|
||||
import { readFixture, WASM_DIR, withBlocks } from './testSupport'
|
||||
import { importedNames, outsideUses, referenceScope, resolveReference, splitReference } from './symbolLinks'
|
||||
import { readFixture, runtime, WASM_DIR, withBlocks } from './testSupport'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
@@ -14,8 +15,16 @@ function symbol(name: string, start: number, end: number, children: SymbolNode[]
|
||||
return { name, range: { startIndex: start, endIndex: end }, selection: { startIndex: start, endIndex: start + name.length }, children }
|
||||
}
|
||||
|
||||
/** 假的语言扩展:几个文件的文档符号,本文件几处名字的定义;记下问过哪些。 */
|
||||
function sources(files: Record<string, SymbolNode[]>, definitions: Record<number, { uri: string, at: number, length: number }> = {}): ReferenceSources & { asked: number[] } {
|
||||
/**
|
||||
* 假的语言扩展:几个文件的文档符号,本文件几处名字的定义,本文件代码里用到、本文件又没定义的名字(名字 → 各处的起点),
|
||||
* 语言自带的名字;记下问过哪些。
|
||||
*/
|
||||
function sources(
|
||||
files: Record<string, SymbolNode[]>,
|
||||
definitions: Record<number, { uri: string, at: number, length: number }> = {},
|
||||
uses: Record<string, number[]> = {},
|
||||
builtins: string[] = [],
|
||||
): ReferenceSources & { asked: number[] } {
|
||||
const asked: number[] = []
|
||||
return {
|
||||
uri: 'file:///here.py',
|
||||
@@ -26,6 +35,8 @@ function sources(files: Record<string, SymbolNode[]>, definitions: Record<number
|
||||
const found = definitions[offset]
|
||||
return found && { uri: found.uri, selection: { startIndex: found.at, endIndex: found.at + found.length } }
|
||||
},
|
||||
uses: name => (uses[name] ?? []).map(start => ({ startIndex: start, endIndex: start + name.length })),
|
||||
builtin: name => builtins.includes(name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +142,132 @@ describe('不带路径的引用:从被注释的声明起一层层往外找(K
|
||||
})
|
||||
})
|
||||
|
||||
describe('不带路径的引用:本文件代码里用到的、语言自带的名字', () => {
|
||||
const imports = [{ name: 'Agent', offset: 20, range: { startIndex: 20, endIndex: 25 } }]
|
||||
const BUILTINS = 'file:///typeshed/builtins.pyi'
|
||||
/** load、save 两个函数,helper 里没用到 ValueError;ValueError 在文件顶层、load 里、save 里各用了一处,Agent 是导入的、代码里也用到了。 */
|
||||
const here = (definitions: Record<number, { uri: string, at: number, length: number }> = {
|
||||
50: { uri: BUILTINS, at: 1000, length: 10 },
|
||||
150: { uri: BUILTINS, at: 1000, length: 10 },
|
||||
450: { uri: BUILTINS, at: 1000, length: 10 },
|
||||
20: { uri: 'file:///models.py', at: 10, length: 5 },
|
||||
}): ReturnType<typeof sources> => sources({
|
||||
'file:///here.py': [symbol('Agent', 20, 25), symbol('load', 100, 300), symbol('save', 400, 600), symbol('helper', 700, 800)],
|
||||
[BUILTINS]: [symbol('ValueError', 1000, 1200, [symbol('args', 1100, 1150)])],
|
||||
}, definitions, { ValueError: [50, 150, 450], Agent: [20, 900] }, ['ValueError', 'str'])
|
||||
const LOAD = 100
|
||||
const SAVE = 400
|
||||
const HELPER = 700
|
||||
const valueError = { uri: BUILTINS, selection: { startIndex: 1000, endIndex: 1010 } }
|
||||
|
||||
it('问写在代码里的那处的定义(同 ⌘ + 点击它):先挑用在这段注释说明的那个声明里面的,没有再挑整个文件的第一处', async () => {
|
||||
const inLoad = here()
|
||||
expect(await resolveReference('ValueError', LOAD, imports, inLoad)).toEqual(valueError)
|
||||
expect(inLoad.asked).toEqual([150])
|
||||
const inSave = here()
|
||||
expect(await resolveReference('ValueError', SAVE, imports, inSave)).toEqual(valueError)
|
||||
expect(inSave.asked).toEqual([450])
|
||||
const elsewhere = here()
|
||||
expect(await resolveReference('ValueError', HELPER, imports, elsewhere)).toEqual(valueError)
|
||||
expect(await resolveReference('ValueError', undefined, imports, elsewhere)).toEqual(valueError)
|
||||
expect(elsewhere.asked).toEqual([50, 50])
|
||||
})
|
||||
|
||||
it('写成 Class.member 的:在定义所在文件的那个类底下找成员,没有这个成员是找不到,不当语言自带的', async () => {
|
||||
expect(await resolveReference('ValueError.args', LOAD, imports, here())).toEqual({ uri: BUILTINS, selection: { startIndex: 1100, endIndex: 1104 } })
|
||||
expect(await resolveReference('ValueError.missing', LOAD, imports, here())).toBeUndefined()
|
||||
})
|
||||
|
||||
it('本文件定义的、导入的照旧先认:导入的名字代码里也用到了,问的还是导入语句里那处', async () => {
|
||||
const found = here()
|
||||
expect(await resolveReference('Agent', LOAD, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 10, endIndex: 15 } })
|
||||
expect(await resolveReference('load', SAVE, imports, found)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 100, endIndex: 104 } })
|
||||
expect(found.asked).toEqual([20])
|
||||
})
|
||||
|
||||
it('代码里没写到的语言自带的名字是 builtin(当普通文字),Class.member 看第一段;不是语言自带的找不到', async () => {
|
||||
const found = here()
|
||||
expect(await resolveReference('str', LOAD, imports, found)).toBe('builtin')
|
||||
expect(await resolveReference('str.join', LOAD, imports, found)).toBe('builtin')
|
||||
expect(await resolveReference('Nowhere', LOAD, imports, found)).toBeUndefined()
|
||||
expect(found.asked).toEqual([])
|
||||
})
|
||||
|
||||
it('代码里写到了、语言扩展却答不上:语言自带的是 builtin,不标成找不到', async () => {
|
||||
expect(await resolveReference('ValueError', LOAD, imports, here({}))).toBe('builtin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('本文件代码里用到、本文件又没定义的名字(取自语言插件认的绑定)', () => {
|
||||
async function usesIn(adapter: LanguageAdapter, languageId: string, source: string): Promise<Map<string, SourceRange[]>> {
|
||||
const tree = await runtime.parse(adapter, languageId, source)
|
||||
try {
|
||||
return outsideUses(adapter.bindings!(tree.rootNode, source, languageId), source)
|
||||
}
|
||||
finally {
|
||||
tree.delete()
|
||||
}
|
||||
}
|
||||
const at = (source: string, text: string, name: string): SourceRange => {
|
||||
const start = source.indexOf(text)
|
||||
return { startIndex: start, endIndex: start + name.length }
|
||||
}
|
||||
|
||||
it('python:内置异常、内置函数照源码顺序记下各处;注释、字符串、属性名里的不算,本文件定义的不在', async () => {
|
||||
const source = [
|
||||
'import os',
|
||||
'',
|
||||
'def load(path):',
|
||||
' """读 path,不对时 @throws [ValueError]"""',
|
||||
' # ValueError 也写在注释里',
|
||||
' if not os.path.exists(path):',
|
||||
' raise FileNotFoundError(path)',
|
||||
' data = config.ValueError',
|
||||
' raise ValueError(len(data))',
|
||||
'',
|
||||
'def check():',
|
||||
' raise ValueError("ValueError")',
|
||||
'',
|
||||
].join('\n')
|
||||
const uses = await usesIn(py, 'python', source)
|
||||
expect(uses.get('ValueError')).toEqual([at(source, 'ValueError(len', 'ValueError'), at(source, 'ValueError("', 'ValueError')])
|
||||
expect(uses.get('FileNotFoundError')).toEqual([at(source, 'FileNotFoundError', 'FileNotFoundError')])
|
||||
expect(uses.get('len')).toEqual([at(source, 'len(', 'len')])
|
||||
expect(['path', 'data', 'load', 'check'].filter(name => uses.has(name))).toEqual([])
|
||||
})
|
||||
|
||||
it('typeScript:throw 的 Error、用到的 JSON 记下;注释、字符串、属性名里的不算,本文件定义的不在', async () => {
|
||||
const source = [
|
||||
'// Error 写在注释里',
|
||||
'export function parse(text: string): unknown {',
|
||||
' const data = JSON.parse(text)',
|
||||
' if (data.Error)',
|
||||
' throw new Error(\'Error\')',
|
||||
' return data',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
const uses = await usesIn(ts, 'typescript', source)
|
||||
expect(uses.get('Error')).toEqual([at(source, 'Error(', 'Error')])
|
||||
expect(uses.get('JSON')).toEqual([at(source, 'JSON', 'JSON')])
|
||||
expect(['parse', 'text', 'data'].filter(name => uses.has(name))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('语言插件给的语言自带的名字', () => {
|
||||
it('python 的是 dir(builtins) 里公开的:内置异常、类型、函数、常量;认异常类照旧只看异常那部分', async () => {
|
||||
expect(['ValueError', 'FileNotFoundError', 'str', 'dict', 'len', 'print', 'None', 'NotImplemented'].filter(name => !py.builtins?.has(name))).toEqual([])
|
||||
expect(['os', 'Path', 'self', '__import__'].filter(name => py.builtins?.has(name))).toEqual([])
|
||||
const exceptions = await withBlocks(py, 'python', 'class A(ValueError):\n pass\n\nclass B(str):\n pass\n', blocks => blocks.map(block => block.kind))
|
||||
expect(exceptions).toEqual(['exception', 'class'])
|
||||
})
|
||||
|
||||
it('typeScript 的是 ECMAScript 标准的全局对象,浏览器、Node 另加的不算', () => {
|
||||
expect(['Error', 'TypeError', 'RangeError', 'Promise', 'Array', 'Map', 'Set', 'Object', 'JSON', 'Math'].filter(name => !ts.builtins?.has(name))).toEqual([])
|
||||
expect(['window', 'document', 'process', 'Buffer'].filter(name => ts.builtins?.has(name))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('注释写在哪块里:从哪个声明起往外找', () => {
|
||||
function block(anchor: string, defines: number | undefined, children: BlockView[] = []): BlockView {
|
||||
return { anchor, children, ...(defines !== undefined && { defines: { startIndex: defines, endIndex: defines + 1 } }) } as BlockView
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { ImportFacts, ImportItem, Node, SourceRange } from './contract'
|
||||
import type { ImportFacts, ImportItem, NameBinding, Node, SourceRange } from './contract'
|
||||
import { findBlock } from './qa/prompt'
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)指到哪:从这段注释说明的那个声明起,一层层往外找本文件定义的,
|
||||
* 再找本文件导入进来的,都没有就找不到。
|
||||
* 再找本文件导入进来的,再找本文件代码里用到的语言自带的名字;语言自带、又问不到定义的当普通文字,别的都没有就找不到。
|
||||
* 符号、定义问语言扩展,交给调用方(预览)去问;这里只管从哪儿起、按什么顺序找、怎么拆名字(不碰 VS Code)。
|
||||
*/
|
||||
|
||||
@@ -29,13 +29,21 @@ export interface LinkTarget {
|
||||
selection: SourceRange
|
||||
}
|
||||
|
||||
/** 解析一个引用要问的:本文件是哪个、它的文档符号,本文件某处名字的定义(几处取第一处),别的文件的文档符号。 */
|
||||
/**
|
||||
* 解析一个引用要问的:本文件是哪个、它的文档符号,本文件某处名字的定义(几处取第一处),别的文件的文档符号,
|
||||
* 本文件代码里用到某个本文件没定义的名字的各处(见 [outsideUses]),某个名字是不是这门语言自带的。
|
||||
*/
|
||||
export interface ReferenceSources {
|
||||
uri: string
|
||||
symbols: (uri: string) => Promise<readonly SymbolNode[]>
|
||||
definition: (offset: number) => Promise<LinkTarget | undefined>
|
||||
uses: (name: string) => readonly SourceRange[]
|
||||
builtin: (name: string) => boolean
|
||||
}
|
||||
|
||||
/** 一个引用解析出来是什么:指到的地方;`'builtin'` 是这门语言自带、又问不到定义的名字,预览当普通文字显示;找不到是 `undefined`。 */
|
||||
export type ReferenceTarget = LinkTarget | 'builtin' | undefined
|
||||
|
||||
function spanOf(nodes: readonly (Node | undefined)[]): SourceRange {
|
||||
const present = nodes.filter((node): node is Node => node !== undefined)
|
||||
return { startIndex: Math.min(...present.map(node => node.startIndex)), endIndex: Math.max(...present.map(node => node.endIndex)) }
|
||||
@@ -63,6 +71,22 @@ export function importedNames(imports: readonly ImportFacts[]): ImportedName[] {
|
||||
return imports.flatMap(facts => facts.items.flatMap(item => importedName(item) ?? []))
|
||||
}
|
||||
|
||||
/**
|
||||
* 本文件代码里用到、本文件又没定义的名字(语言自带的、导入的,见 [NameBinding] 的 `local`)各在哪几处:名字 → 各处的范围,照源码顺序。
|
||||
* 绑定是语言插件照语法树认的,注释、字符串里的字和 `obj.name` 里的 `name` 都不在里面;导入的名字含导入语句里那一处。
|
||||
*/
|
||||
export function outsideUses(bindings: readonly NameBinding[], source: string): Map<string, SourceRange[]> {
|
||||
const uses = new Map<string, SourceRange[]>()
|
||||
for (const { occurrences, local } of bindings) {
|
||||
const [first] = occurrences
|
||||
if (local || !first)
|
||||
continue
|
||||
const name = source.slice(first.startIndex, first.endIndex)
|
||||
uses.set(name, [...uses.get(name) ?? [], ...occurrences].sort((a, b) => a.startIndex - b.startIndex))
|
||||
}
|
||||
return uses
|
||||
}
|
||||
|
||||
/** 引用拆成名字和成员:`Agent.owner_id` 是 Agent 和 [owner_id]。 */
|
||||
export function splitReference(reference: string): { head: string, members: string[] } {
|
||||
const [head = '', ...members] = reference.split('.')
|
||||
@@ -107,24 +131,11 @@ function definedNear(symbols: readonly SymbolNode[], at: number | undefined, nam
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用指到哪,按 KDoc 的规矩(不带限定的名字,当它写在这段注释说明的那个声明里面来认):
|
||||
* 1. 本文件定义的,由近到远一层层找:at 是注释说明的那个声明的名字在源码里的下标(见 referenceScope)。先看盖住 at 的最里层符号
|
||||
* (就是这个声明)的直接子符号,再一层层往外看包着它的符号的直接子符号,最后看文件顶层;哪层先有这个名字(`Class.member` 看第一段)
|
||||
* 就是哪个,`Class.member` 再在它底下找成员。导入语句里的名字不算本文件定义的。没有 at(不在哪个声明的注释里)只看文件顶层。
|
||||
* 2. 本文件导入进来的:名字(`Class.member` 的第一段)是某条导入引进来的,问导入语句里那个名字的定义;`Class.member` 再在定义所在文件的
|
||||
* 符号里找:定义落在哪个符号里就在它底下找,落在文件开头(导入的是模块)就从顶层找。
|
||||
* 3. 都没有:找不到。本文件某层有这个名字、却没有这个成员时也是找不到,不再去更外层、导入里找。
|
||||
* 问到的定义 `target` 底下的成员 `members`,没有成员就是 `target` 自己:在定义所在文件的符号里找,定义落在哪个符号里就在它底下找,
|
||||
* 落在文件开头(导入的是模块)就从顶层找。
|
||||
*/
|
||||
export async function resolveReference(reference: string, at: number | undefined, imports: readonly ImportedName[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
|
||||
const { head, members } = splitReference(reference)
|
||||
const own = definedNear(await sources.symbols(sources.uri), at, head, imports)
|
||||
if (own) {
|
||||
const found = members.length > 0 ? memberOf(own.children, members) : own
|
||||
return found && { uri: sources.uri, selection: found.selection }
|
||||
}
|
||||
const imported = imports.find(item => item.name === head)
|
||||
const target = imported && await sources.definition(imported.offset)
|
||||
if (!target || members.length === 0)
|
||||
async function memberAt(target: LinkTarget, members: readonly string[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
|
||||
if (members.length === 0)
|
||||
return target
|
||||
const symbols = await sources.symbols(target.uri)
|
||||
const holder = enclosing(symbols, target.selection.startIndex).at(-1)
|
||||
@@ -132,6 +143,43 @@ export async function resolveReference(reference: string, at: number | undefined
|
||||
return found && { uri: target.uri, selection: found.selection }
|
||||
}
|
||||
|
||||
/** 名字用在代码里的各处 `uses` 里挑一处问定义:先挑用在 `at` 那个声明(盖住 `at` 的最里层符号)里面的第一处,没有再挑整个文件的第一处。 */
|
||||
function useNear(symbols: readonly SymbolNode[], at: number | undefined, uses: readonly SourceRange[]): SourceRange | undefined {
|
||||
const declared = at === undefined ? undefined : enclosing(symbols, at).at(-1)
|
||||
return (declared && uses.find(range => covers(declared.range, range.startIndex))) ?? uses[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用指到哪,按 KDoc 的规矩(不带限定的名字,当它写在这段注释说明的那个声明里面来认):
|
||||
* 1. 本文件定义的,由近到远一层层找:`at` 是注释说明的那个声明的名字在源码里的下标(见 [referenceScope])。先看盖住 `at` 的最里层符号
|
||||
* (就是这个声明)的直接子符号,再一层层往外看包着它的符号的直接子符号,最后看文件顶层;哪层先有这个名字(`Class.member` 看第一段)
|
||||
* 就是哪个,`Class.member` 再在它底下找成员。导入语句里的名字不算本文件定义的。没有 `at`(不在哪个声明的注释里)只看文件顶层。
|
||||
* 2. 本文件导入进来的:名字(第一段)是某条导入引进来的,问导入语句里那个名字的定义;`Class.member` 再在定义所在文件的符号里找成员(见 [memberAt])。
|
||||
* 3. 本文件代码里用到、本文件又没定义的(语言自带的 `ValueError`、`str`……):在名字(第一段)用在代码里的各处([ReferenceSources.uses])挑一处
|
||||
* 问定义(见 [useNear]),同在代码里 ⌘ + 点击它,Python 的内置名跳到 Pylance 自带的 builtins.pyi;`Class.member` 同第 2 步再找成员。
|
||||
* 4. 前面都没问到定义、名字(第一段)又是这门语言自带的([ReferenceSources.builtin]):是 `'builtin'`,预览当普通文字,不标成找不到。
|
||||
* 5. 都不是就找不到。本文件某层有这个名字、或问到了定义,却没有这个成员时也是找不到,不再往后找。
|
||||
*/
|
||||
export async function resolveReference(reference: string, at: number | undefined, imports: readonly ImportedName[], sources: ReferenceSources): Promise<ReferenceTarget> {
|
||||
const { head, members } = splitReference(reference)
|
||||
const symbols = await sources.symbols(sources.uri)
|
||||
const own = definedNear(symbols, at, head, imports)
|
||||
if (own) {
|
||||
const found = members.length > 0 ? memberOf(own.children, members) : own
|
||||
return found && { uri: sources.uri, selection: found.selection }
|
||||
}
|
||||
const imported = imports.find(item => item.name === head)
|
||||
if (imported) {
|
||||
const target = await sources.definition(imported.offset)
|
||||
return target && memberAt(target, members, sources)
|
||||
}
|
||||
const use = useNear(symbols, at, sources.uses(head))
|
||||
const target = use && await sources.definition(use.startIndex)
|
||||
if (target)
|
||||
return memberAt(target, members, sources)
|
||||
return sources.builtin(head) ? 'builtin' : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释写在 anchor 那块里时,引用从哪个声明起找(resolveReference 的 at):这块有名字(是个声明)就是它的名字在源码里的下标;
|
||||
* 没名字的(注释、语句……)取外层最近的有名字的块。外层也都没名字、块树里没有这块时没有,只从文件顶层找。
|
||||
|
||||
Reference in new issue
Block a user