🐛 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
@@ -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)再问一次', () => {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user