🐛 fix(vscode): 代码阅读注释里引用的语言内置名不再标成找不到:代码里写到的(raise ValueError)问那处的定义、点了跳到语言自带的定义,没写到的当普通文字不能点;内置名单由语言插件给(Python 补全 dir(builtins),TypeScript 是 ECMAScript 标准全局对象)

This commit is contained in:
cruldra committed 2026-10-14 15:10:48 +11:00
1 parent 49a3854390
commit e3c8c1b118
14 files changed
+544 -98

No files matched your search

@@ -260,6 +260,13 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-colored { color: var(--c
text-decoration: underline wavy;
text-underline-offset: 2px;
}
/* 不带路径的引用是语言自带的名字(Python 的 ValueError、str),跳不过去:和旁边的字一样,不像链接 */
.cr-markdown a.cr-link-plain,
.cr-markdown a.cr-link-plain:hover {
color: inherit;
text-decoration: none;
cursor: text;
}
/* 仓库图片:按原图比例,宽不超过注释区。点了是仓库链接,打开原图 */
.cr-markdown .cr-repo-image img {
max-width: 100%;
@@ -94,11 +94,11 @@ describe('repoLinks', () => {
links.scan([link('backend/a.py#Agent'), link('vendor/agents')])
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py', 'vendor/agents'], references: [] }])
expect(links.checked(0, ['vendor/agents'], [])).toBe(false)
expect(links.isBroken(link('vendor/agents'))).toBe(false)
expect(links.checked(1, ['vendor/agents'], [])).toBe(true)
expect(links.isBroken(link('vendor/agents'))).toBe(true)
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(false)
expect(links.checked(0, ['vendor/agents'], [], [])).toBe(false)
expect(links.state(link('vendor/agents'))).toBe('link')
expect(links.checked(1, ['vendor/agents'], [], [])).toBe(true)
expect(links.state(link('vendor/agents'))).toBe('broken')
expect(links.state(link('backend/a.py#Agent'))).toBe('link')
})
it('带几行的链接:点了发 openRepoLink 带上这几行;文件在不在和别的仓库路径一起问,文件没有就标坏', () => {
@@ -108,18 +108,18 @@ describe('repoLinks', () => {
links.scan([link('backend/app/service.py#L357-L364'), link('backend/app/service.py#L3'), link('web/a.ts')])
expect(sent.at(-1)).toEqual({ type: 'checkRepoLinks', id: 1, paths: ['backend/app/service.py', 'web/a.ts'], references: [] })
links.checked(1, ['backend/app/service.py'], [])
expect(links.isBroken(link('backend/app/service.py#L3'))).toBe(true)
expect(links.isBroken(link('web/a.ts'))).toBe(false)
links.checked(1, ['backend/app/service.py'], [], [])
expect(links.state(link('backend/app/service.py#L3'))).toBe('broken')
expect(links.state(link('web/a.ts'))).toBe('link')
})
it('符号没有才标坏;打开成功就不再标', () => {
const { links } = setup()
links.broken({ path: 'backend/a.py', symbol: 'Agent' })
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(true)
expect(links.isBroken(link('backend/a.py'))).toBe(false)
expect(links.state(link('backend/a.py#Agent'))).toBe('broken')
expect(links.state(link('backend/a.py'))).toBe('link')
links.opened({ path: 'backend/a.py', symbol: 'Agent' })
expect(links.isBroken(link('backend/a.py#Agent'))).toBe(false)
expect(links.state(link('backend/a.py#Agent'))).toBe('link')
})
})
@@ -140,13 +140,13 @@ describe('不带路径的引用', () => {
links.scan([{ symbol: 'USER', block: 'a' }, link('backend/a.py'), { symbol: 'Missing', block: 'a' }, { symbol: 'USER', block: 'a' }])
expect(sent.at(-1)).toEqual({ type: 'checkRepoLinks', id: 1, paths: ['backend/a.py'], references: [{ symbol: 'Missing', block: 'a' }, { symbol: 'USER', block: 'a' }] })
expect(links.checked(1, [], [{ symbol: 'Missing', block: 'a' }])).toBe(true)
expect(links.isBroken({ symbol: 'Missing', block: 'a' })).toBe(true)
expect(links.isBroken({ symbol: 'USER', block: 'a' })).toBe(false)
expect(links.checked(1, [], [{ symbol: 'Missing', block: 'a' }], [])).toBe(true)
expect(links.state({ symbol: 'Missing', block: 'a' })).toBe('broken')
expect(links.state({ symbol: 'USER', block: 'a' })).toBe('link')
links.opened({ symbol: 'Missing', block: 'a' })
expect(links.isBroken({ symbol: 'Missing', block: 'a' })).toBe(false)
expect(links.state({ symbol: 'Missing', block: 'a' })).toBe('link')
links.broken({ symbol: 'USER', block: 'a' })
expect(links.isBroken({ symbol: 'USER', block: 'a' })).toBe(true)
expect(links.state({ symbol: 'USER', block: 'a' })).toBe('broken')
})
it('同一个名字写在两块的注释里:分开问,一块找得到一块找不到时各标各的', () => {
@@ -157,16 +157,35 @@ describe('不带路径的引用', () => {
links.scan([inMethod, inFunction, outside, inMethod])
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: [], references: [outside, inMethod, inFunction] }])
links.checked(1, [], [inFunction, outside])
expect(links.isBroken(inMethod)).toBe(false)
expect(links.isBroken(inFunction)).toBe(true)
expect(links.isBroken(outside)).toBe(true)
links.checked(1, [], [inFunction, outside], [])
expect(links.state(inMethod)).toBe('link')
expect(links.state(inFunction)).toBe('broken')
expect(links.state(outside)).toBe('broken')
links.opened(inFunction)
links.broken(inMethod)
expect(links.isBroken(inFunction)).toBe(false)
expect(links.isBroken(inMethod)).toBe(true)
expect(links.isBroken(outside)).toBe(true)
expect(links.state(inFunction)).toBe('link')
expect(links.state(inMethod)).toBe('broken')
expect(links.state(outside)).toBe('broken')
})
it('语言自带的名字当普通文字、点了不发 openRepoLink;点了才知道是语言自带的(repoLinkBuiltin)也改当普通文字,不再标坏', () => {
const { links, sent } = setup()
const thrown = { symbol: 'ValueError', block: 'function load' }
const typed = { symbol: 'str', block: 'function load' }
links.scan([thrown, typed])
expect(links.checked(1, [], [typed], [thrown])).toBe(true)
expect(links.state(thrown)).toBe('plain')
expect(links.state(typed)).toBe('broken')
expect(links.state({ symbol: 'ValueError', block: 'function other' })).toBe('link')
expect(links.click(thrown)).toBe(true)
expect(sent.filter(msg => msg.type === 'openRepoLink')).toEqual([])
links.builtin(typed)
expect(links.state(typed)).toBe('plain')
links.opened(typed)
expect(links.state(typed)).toBe('link')
})
it('同样的链接不再问;文档换了版本(stale)再问一次', () => {
+35 -16
View File
@@ -145,9 +145,12 @@ function referenceKey({ symbol, block }: SymbolReference): string {
return `${symbol}\u0000${block ?? ''}`
}
/** 页面上一个链接眼下怎么显示:照常的链接;坏的(文件、符号没有,引用找不到);普通文字(语言自带、又问不到定义的名字,点了不打开)。 */
export type LinkState = 'link' | 'broken' | 'plain'
/**
* 页面上的仓库链接和不带路径的引用:点了发给扩展端打开;预览出来后把路径、引用发去问哪些文件没有、哪些引用找不到;
* 扩展端说没有的,标成坏的。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪块,名字)分开问、分开标。
* 页面上的仓库链接和不带路径的引用:点了发给扩展端打开;预览出来后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;
* 扩展端说没有的标成坏的,说是语言自带的当普通文字。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪块,名字)分开问、分开标。
*/
export class RepoLinks {
private request = 0
@@ -155,16 +158,19 @@ export class RepoLinks {
private sent = ''
private missingPaths = new Set<string>()
private missingSymbols = new Set<string>()
/** 找不到的不带路径的引用(referenceKey) */
/** 找不到的不带路径的引用([referenceKey]) */
private unresolved = new Set<string>()
/** 语言自带、又问不到定义的不带路径的引用([referenceKey]):当普通文字 */
private builtins = new Set<string>()
constructor(private readonly post: (msg: WebviewToExtension) => void) {}
/** 点在仓库链接或不带路径的引用上(见 commentLinkOf):发给扩展端打开,返回 true。别的链接(没认出来的)不管。 */
/** 点在仓库链接或不带路径的引用上(见 [commentLinkOf]):发给扩展端打开,返回 `true`;当普通文字的不发,也返回 `true`,免得被当外部链接打开。别的链接(没认出来的)不管。 */
click(link: CommentLink | undefined): boolean {
if (!link)
return false
this.post({ type: 'openRepoLink', ...link })
if (this.state(link) !== 'plain')
this.post({ type: 'openRepoLink', ...link })
return true
}
@@ -192,20 +198,23 @@ export class RepoLinks {
this.sent = ''
}
/** id 那次询问的回答。不是最近一次的丢掉,返回这次有没有用上。 */
checked(id: number, missing: readonly string[], unresolved: readonly SymbolReference[]): boolean {
/** `id` 那次询问的回答:仓库里没有的路径 `missing`,找不到的引用 `unresolved`,语言自带的名字 `builtin`。不是最近一次的丢掉,返回这次有没有用上。 */
checked(id: number, missing: readonly string[], unresolved: readonly SymbolReference[], builtin: readonly SymbolReference[]): boolean {
if (id !== this.request)
return false
this.missingPaths = new Set(missing)
this.unresolved = new Set(unresolved.map(referenceKey))
this.builtins = new Set(builtin.map(referenceKey))
return true
}
/** 扩展端打开时发现文件或符号没有;没有路径的是写在 block 那块里的不带路径的引用找不到。 */
broken({ path, symbol, block }: ReportedLink): void {
if (path === undefined) {
if (symbol !== undefined)
if (symbol !== undefined) {
this.unresolved.add(referenceKey({ symbol, block }))
this.builtins.delete(referenceKey({ symbol, block }))
}
return
}
if (symbol !== undefined)
@@ -217,8 +226,10 @@ export class RepoLinks {
/** 打开成功:这个路径在,这个符号也在;没有路径的是写在 block 那块里的不带路径的引用找到了。 */
opened({ path, symbol, block }: ReportedLink): void {
if (path === undefined) {
if (symbol !== undefined)
if (symbol !== undefined) {
this.unresolved.delete(referenceKey({ symbol, block }))
this.builtins.delete(referenceKey({ symbol, block }))
}
return
}
this.missingPaths.delete(path)
@@ -226,14 +237,22 @@ export class RepoLinks {
this.missingSymbols.delete(symbolKey(path, symbol))
}
/** 这个链接现在是不是坏的(文件没有,这个符号没有,或写在这块里的引用找不到)。还没问到的不算。 */
isBroken(link: CommentLink | undefined): boolean {
/** 扩展端打开时发现写在 `block` 那块里的不带路径的引用是语言自带、又问不到定义的名字:改当普通文字,不再标坏。 */
builtin(reference: SymbolReference): void {
this.unresolved.delete(referenceKey(reference))
this.builtins.add(referenceKey(reference))
}
/** 这个链接现在怎么显示(见 [LinkState]):文件没有、这个符号没有、写在这块里的引用找不到是坏的,写在这块里的引用是语言自带的名字是普通文字。还没问到的照常是链接。 */
state(link: CommentLink | undefined): LinkState {
if (!link)
return false
if (link.path === undefined)
return this.unresolved.has(referenceKey(link))
return 'link'
if (link.path === undefined) {
const key = referenceKey(link)
return this.unresolved.has(key) ? 'broken' : this.builtins.has(key) ? 'plain' : 'link'
}
if (this.missingPaths.has(link.path))
return true
return link.symbol !== undefined && this.missingSymbols.has(symbolKey(link.path, link.symbol))
return 'broken'
return link.symbol !== undefined && this.missingSymbols.has(symbolKey(link.path, link.symbol)) ? 'broken' : 'link'
}
}
+5 -2
View File
@@ -536,9 +536,11 @@ export interface CodeColors {
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
* diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了
* openedFrom:这个标签页是从哪儿打开的,记进页面状态;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 这份文档里,每个能看调用树的块落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
@@ -606,8 +608,9 @@ export type ExtensionToWebview
| RewriteEnd
| { 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' }
@@ -1,3 +1,4 @@
import type { LinkState } from './repoLink'
import { useEffect, useState } from 'react'
import { commentLinkOf, RepoLinks } from './repoLink'
import { onMessage, postMessage } from './vscode'
@@ -5,10 +6,14 @@ import { onMessage, postMessage } from './vscode'
/** 坏链接悬停上的字,走页面上统一的 data-tooltip,不用原生 title。 */
const MISSING = '找不到'
/** 文件或符号没有的仓库链接、找不到的不带路径的引用:红色波浪线,悬停「找不到」。问到之前先是普通链接。 */
function paintLink(anchor: HTMLAnchorElement, broken: boolean): void {
/**
* 按链接眼下的状态(见 [LinkState])画:文件或符号没有的仓库链接、找不到的不带路径的引用是红色波浪线,悬停「找不到」;
* 语言自带的名字画成普通文字。问到之前先是普通链接。
*/
function paintLink(anchor: HTMLAnchorElement, state: LinkState): void {
anchor.classList.toggle('cr-link-plain', state === 'plain')
const tip = anchor.getAttribute('data-tooltip')
if (broken) {
if (state === 'broken') {
anchor.classList.add('cr-link-broken')
if (tip !== MISSING) {
if (tip)
@@ -32,12 +37,12 @@ function paintLink(anchor: HTMLAnchorElement, broken: boolean): void {
function paint(links: RepoLinks): void {
for (const anchor of document.querySelectorAll<HTMLAnchorElement>('.cr-markdown a'))
paintLink(anchor, links.isBroken(commentLinkOf(anchor)))
paintLink(anchor, links.state(commentLinkOf(anchor)))
}
/**
* 注释、说明、问 AI 的回答里的仓库路径链接,和注释里不带路径的引用:点了发给扩展端打开(不走 webview 把 http 链接交给外部打开的那条),
* 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到;文档换了版本再问一次(引用跟着源码变)。
* 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;文档换了版本再问一次(引用跟着源码变)。
* 不带路径的引用都带着它写在哪块(见 commentLinkOf),扩展端从那块的声明起找。
*/
export function useRepoLinks(): void {
@@ -50,7 +55,7 @@ export function useRepoLinks(): void {
timer = window.setTimeout(() => {
const found = [...document.querySelectorAll<HTMLAnchorElement>('.cr-markdown a')].map(anchor => ({ anchor, link: commentLinkOf(anchor) }))
for (const { anchor, link } of found)
paintLink(anchor, links.isBroken(link))
paintLink(anchor, links.state(link))
links.scan(found.flatMap(({ link }) => link ?? []))
}, 50)
}
@@ -61,10 +66,15 @@ export function useRepoLinks(): void {
return
}
if (msg.type === 'repoLinksChecked') {
if (links.checked(msg.id, msg.missing, msg.unresolved))
if (links.checked(msg.id, msg.missing, msg.unresolved, msg.builtin))
paint(links)
return
}
if (msg.type === 'repoLinkBuiltin') {
links.builtin(msg)
paint(links)
return
}
if (msg.type === 'repoLinkBroken') {
links.broken(msg)
paint(links)