🐛 fix(vscode): 定义卡片里注释的 [Name] 渲染成链接,按卡片那份文件解析

This commit is contained in:
cruldra committed 2026-10-15 17:36:50 +11:00
1 parent af91b03857
commit bea1f2e679
19 files changed
+319 -151

No files matched your search

@@ -1,7 +1,12 @@
// @vitest-environment happy-dom
import type { BlockView, DefinitionPeek } from './types'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { PeekTree } from './DefinitionPeekPopup'
import { DefinitionPeekPopup, PeekTree } from './DefinitionPeekPopup'
import { commentLinkOf } from './repoLink'
import { NO_SPAN } from './testBlocks'
vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } }))
@@ -24,6 +29,7 @@ const NAME = 'PairedDeviceRepository'
const PEEK: DefinitionPeek = {
path: 'app/devices.py',
uri: 'file:///w/app/devices.py',
line: 41,
block: block('3', 'class', {
commentStartLine: 42,
@@ -57,8 +63,27 @@ describe('定义卡片浮窗里的卡片', () => {
expect(html).toContain(`<span class="cr-original">${NAME}</span>`)
})
it('注释里不带路径的引用不成链接:它按注释所在的文件找,页面只替自己这份文件问', () => {
expect(html).not.toContain('symbol:DesktopOnly')
expect(html).toContain('[DesktopOnly]')
it('注释里不带路径的引用同页面上一样是链接', () => {
expect(html).toContain('href="symbol:DesktopOnly"')
expect(html).not.toContain('[DesktopOnly]')
})
})
describe('定义卡片浮窗', () => {
it('浮窗根上记着卡片那份文件:卡片注释里不带路径的引用带着它和写在的那块,扩展端按那份文件找', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
const anchor = document.createElement('span')
document.body.append(anchor)
const root = createRoot(document.createElement('div'))
try {
await act(async () => root.render(<DefinitionPeekPopup shown={{ anchor, peek: PEEK, jump: { type: 'openRepoLink', symbol: NAME } }} onJump={() => {}} />))
const link = document.querySelector<HTMLAnchorElement>('.cr-peek a[href="symbol:DesktopOnly"]')!
expect(commentLinkOf(link)).toEqual({ symbol: 'DesktopOnly', block: '3', file: 'file:///w/app/devices.py' })
}
finally {
await act(async () => root.unmount())
anchor.remove()
vi.unstubAllGlobals()
}
})
})
@@ -10,7 +10,6 @@ import { nameMap } from './displayNames'
import { placementOf } from './placement'
import { TextMarksContext } from './SegmentText'
import { createStore, staticStore } from './store'
import { MarkdownKindContext } from './TextPreview'
import { collapsedIds } from './viewState'
const NONE: ReadonlySet<string> = new Set()
@@ -18,7 +17,7 @@ const NOT_COVERED: ReadonlyMap<string, string> = new Map()
/**
* 浮窗里的那张卡片:只读的块树(不编辑、不问 AI、不重写注释、不标差异、不筛选),折叠展开只记在浮窗里,默认同页面(类、函数收起子块)。
* 名字的下标是那个文件的:中文名换成卡片带来的,不画页面上的高亮;Markdown 不认不带路径的引用。
* 名字的下标是那个文件的:中文名换成卡片带来的,不画页面上的高亮。注释里不带路径的引用同页面上一样是链接,按那个文件找(见 [DefinitionPeekPopup])。
*/
export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement {
const [tree] = useState((): BlockTreeHandle => {
@@ -43,9 +42,7 @@ export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement {
const marks = useMemo(() => staticStore({ names: nameMap(peek.names), placement: placementOf(blocks) }), [peek, blocks])
return (
<TextMarksContext value={marks}>
<MarkdownKindContext value="peek">
<BlockTree blocks={blocks} tree={tree} />
</MarkdownKindContext>
<BlockTree blocks={blocks} tree={tree} />
</TextMarksContext>
)
}
@@ -53,6 +50,7 @@ export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement {
/**
* 定义卡片浮窗:标题一行「相对路径:行号」,点了跳到定义(同 ⌘ / Ctrl + 点击);下面是卡片,太大时在浮窗里滚。
* 外框用 VS Code 悬停框的颜色;portal 到 body,先在左上角量出大小,再落到名字下方或上方(placePeek)。
* 根上记着卡片那份文件([PEEK_ATTRIBUTE]),卡片注释里不带路径的引用点了、标坏都按那份文件找。
*/
export function DefinitionPeekPopup({ shown, onJump }: { shown: ShownPeek<HTMLElement> | undefined, onJump: () => void }): ReactElement | null {
const popupRef = useRef<HTMLDivElement | null>(null)
@@ -80,7 +78,7 @@ export function DefinitionPeekPopup({ shown, onJump }: { shown: ShownPeek<HTMLEl
return createPortal(
<div
ref={popupRef}
{...{ [PEEK_ATTRIBUTE]: '' }}
{...{ [PEEK_ATTRIBUTE]: peek.uri }}
role="dialog"
aria-label={`定义 ${where}`}
className="cr-peek"
@@ -11,7 +11,7 @@ import { DefinitionPeeks, linkPeekSource } from './definitionPeek'
import { DiffText } from './DiffText'
import { commentLinkOf } from './repoLink'
import { hashed, NO_SPAN } from './testBlocks'
import { MarkdownKindContext, TextPreview } from './TextPreview'
import { TextPreview } from './TextPreview'
let container: HTMLDivElement
let root: Root
@@ -134,6 +134,7 @@ describe.each(['TextPreview', 'DiffText'] as const)('%s 的 Markdown 页面元
expect(post).toHaveBeenLastCalledWith({ type: 'peekLink', id: 1, symbol: 'AsyncOpenAI' })
const peek: DefinitionPeek = {
path: 'client.py',
uri: 'file:///w/client.py',
line: 1,
block: hashed([{ id: '1', anchor: '1', kind: 'class', span: NO_SPAN, attributes: { kind: 'class' }, concepts: [], children: [], translation: [] }])[0],
names: [],
@@ -147,22 +148,6 @@ describe.each(['TextPreview', 'DiffText'] as const)('%s 的 Markdown 页面元
})
})
it('组件 TextPreview 随注释与定义预览模式切换更新相同正文的链接', async () => {
const preview = (kind: 'comment' | 'peek'): ReactElement => (
<MarkdownKindContext value={kind}>
<TextPreview text={{ markdown: '[Client]' }} />
</MarkdownKindContext>
)
await render(preview('comment'))
const original = anchor()
await render(preview('peek'))
expect(container.querySelector('a')).toBeNull()
expect(container.textContent).toBe('[Client]\n')
expect(original.isConnected).toBe(false)
await render(preview('comment'))
expect(anchor().getAttribute('href')).toBe('symbol:Client')
})
it('组件 DiffText 更新正文时保留 Markdown 排版和增删标记', async () => {
await render(<DiffText diff={{ markdown: [{ text: '[Client] ' }, { text: '旧说明', change: 'del' }, { text: '新说明', change: 'add' }] }} />)
expect(anchor().textContent).toBe('Client')
@@ -1,22 +1,17 @@
import type { ReactElement } from 'react'
import type { MarkdownKind } from './markdown'
import type { PreviewText } from './types'
import { createContext, memo, useContext, useMemo } from 'react'
import { memo, useMemo } from 'react'
import { cn } from '@/lib/utils'
import { renderMarkdown } from './markdown'
import { SegmentText } from './SegmentText'
/** 卡片里的 Markdown 说明按哪种渲染:页面上是注释;定义卡片浮窗里是 peek(别的文件的注释,不认不带路径的引用)。 */
export const MarkdownKindContext = createContext<Exclude<MarkdownKind, 'answer'>>('comment')
/**
* 预览里的一段文字:着色的词按源码排版;Markdown 说明渲染成段落、列表、代码块。
* `find` 是卡片上这段字的记号(见 [textKey](vscode/webview-ui/src/codeReader/viewState.ts#textKey)),查找栏按它找到这段字画高亮。
*/
export const TextPreview = memo(({ text, className, find }: { text: PreviewText, className?: string, find?: string }): ReactElement => {
const kind = useContext(MarkdownKindContext)
if ('markdown' in text)
return <MarkdownPreview html={renderMarkdown(text.markdown, kind)} className={className} find={find} />
return <MarkdownPreview html={renderMarkdown(text.markdown)} className={className} find={find} />
return (
<p className={cn('cr-code', className)} data-find={find}>
<SegmentText segments={text} />
@@ -1,10 +1,13 @@
import type { CommentLink } from './repoLink'
import type { DefinitionPeek, WebviewToExtension } from './types'
/** 定义卡片浮窗的根上的属性:浮窗里的名字、链接是别的文件的,页面上管名字的那几处(跳到定义、高亮各处出现、再弹卡片)都不认。 */
/**
* 定义卡片浮窗的根上的属性,值是卡片那份文件([DefinitionPeek] 的 `uri`):浮窗里的名字、链接是那份文件的,页面上管名字的那几处
* (跳到定义、高亮各处出现、再弹卡片)都不认;注释里不带路径的引用带上它,扩展端按那份文件找(见 commentLinkOf)。
*/
export const PEEK_ATTRIBUTE = 'data-peek'
/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用,后者带着写在哪块)。 */
/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用,后者带着写在哪块、哪份文件)。 */
export type PeekSource = { offset: number } | (CommentLink & { symbol: string })
/** 指针下能弹定义卡片的一处:页面上的哪个元素(浮窗挂在它旁边),它是什么。 */
@@ -110,11 +110,6 @@ describe('不带路径的引用', () => {
expect(renderMarkdown('见 [Agent]', 'answer')).toBe('<p>见 [Agent]</p>\n')
expect(renderMarkdownDiff([{ text: '见 ' }, { text: '[Agent]', change: 'add' }])).toContain('href="symbol:Agent"')
})
it('定义卡片浮窗里别的文件的注释不认(引用按注释所在的文件找);仓库路径链接照样是链接', () => {
expect(renderMarkdown('见 [Agent]', 'peek')).toBe('<p>见 [Agent]</p>\n')
expect(renderMarkdown('见 [Agent](app/models.py#Agent)', 'peek')).toContain('<a href="app/models.py#Agent">')
})
})
describe('renderMarkdownDiff', () => {
@@ -141,7 +136,6 @@ describe('mermaid 代码块', () => {
it('出占位:转义后的源码记在 data-source,里面先放原样的代码块', () => {
expect(renderMarkdown(`说明\n\n\`\`\`mermaid\n${source}\`\`\``)).toBe(`<p>说明</p>\n${placeholder}`)
expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'peek')).toBe(placeholder)
expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'answer')).toBe(placeholder)
})
+3 -3
View File
@@ -126,10 +126,10 @@ const CACHE_SIZE = 500
const cache = new Map<string, string>()
/**
* 渲染的是什么:注释(说明、注释块,认不带路径的引用),定义卡片浮窗里的注释(不认不带路径的引用:引用按注释所在的文件找,
* 页面只替自己这份文件问),还是问 AI 的回答。
* 渲染的是什么:注释(说明、注释块,认不带路径的引用;定义卡片浮窗里别的文件的注释也是,引用按它写在的那份文件找,见 commentLinkOf),
* 还是问 AI 的回答。
*/
export type MarkdownKind = 'comment' | 'peek' | 'answer'
export type MarkdownKind = 'comment' | 'answer'
function envOf(kind: MarkdownKind, diagrams: boolean): RenderEnv {
return { symbolLinks: kind === 'comment', codeLinks: kind === 'answer', diagrams }
@@ -85,7 +85,7 @@ async function peekOf(source: string, needle: string, name: string): Promise<Ext
const offset = source.indexOf(needle)
const line = source.slice(0, offset).split('\n').length - 1
const { blocks } = await read(source, 1)
return { path: 'other.py', line, block: pagePeekBlock(peekBlock(blocks, offset, line)!, source), names: [{ offset, length: needle.length, name }] }
return { path: 'other.py', uri: 'file:///w/other.py', line, block: pagePeekBlock(peekBlock(blocks, offset, line)!, source), names: [{ offset, length: needle.length, name }] }
}
/** source 里第 nth 个 needle 的下标、所在的行 */
@@ -201,6 +201,27 @@ describe('不带路径的引用', () => {
])
})
it('同一个名字、同一个块记号写在不同文件的注释里(定义卡片里别的文件的):分开问、各标各的,点了和扩展端报回的都带着那份文件', () => {
const { links, sent } = setup()
const onPage = { symbol: 'tauriBridge', block: 'function useShellEvents' }
const inCard = { ...onPage, file: 'file:///w/use-shell-events.ts' }
links.scan([onPage, inCard])
expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: [], references: [onPage, inCard] }])
links.checked(1, [], [onPage], [])
expect(links.state(onPage)).toBe('broken')
expect(links.state(inCard)).toBe('link')
expect(links.click(inCard)).toBe(true)
expect(sent.at(-1)).toEqual({ type: 'openRepoLink', ...inCard })
links.broken(inCard)
links.opened(onPage)
expect(links.state(inCard)).toBe('broken')
expect(links.state(onPage)).toBe('link')
links.builtin(inCard)
expect(links.state(inCard)).toBe('plain')
expect(links.state(onPage)).toBe('link')
})
it('名字一样、写在另一块里的也算变了,要再问', () => {
const { links, sent } = setup()
links.scan([{ symbol: 'USER', block: 'a' }])
@@ -231,6 +252,19 @@ describe('页面上的链接写在哪块的注释里', () => {
expect(at('outside')).toEqual({ symbol: 'get_agent_admitted' })
expect(at('web')).toBeUndefined()
})
it('定义卡片浮窗里的不带路径的引用再带上卡片那份文件(浮窗根上记着的);浮窗里的仓库路径链接不带', () => {
document.body.innerHTML = `
<div data-peek="file:///w/use-shell-events.ts">
<div class="cr-block" data-anchor="function useShellEvents">
<div class="cr-markdown"><a id="card" href="symbol:tauriBridge">tauriBridge</a><a id="path" href="web/a.ts#load">load</a></div>
</div>
</div>
`
const at = (id: string): CommentLink | undefined => commentLinkOf(document.getElementById(id)!)
expect(at('card')).toEqual({ symbol: 'tauriBridge', block: 'function useShellEvents', file: 'file:///w/use-shell-events.ts' })
expect(at('path')).toEqual({ path: 'web/a.ts', symbol: 'load' })
})
})
describe('行内代码里写的代码位置', () => {
+25 -21
View File
@@ -1,4 +1,5 @@
import type { LineRange, SymbolReference, WebviewToExtension } from './types'
import { PEEK_ATTRIBUTE } from './definitionPeek'
/** 注释、问 AI 的回答里指向仓库代码的链接:从仓库根起的路径,可选一个符号或几行。 */
export interface RepoLink {
@@ -11,8 +12,8 @@ export interface RepoLink {
}
/**
* 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按当前文件、从它写在的那块(block,见 commentLinkOf)找这个符号。
* 页面上它是地址为 `symbol:Name` 的链接(见 symbolHref)。
* 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按它写在的那份文件(file,定义卡片里的才有,没有是当前文件)、
* 从它写在的那块(block)找这个符号,两样都见 commentLinkOf。页面上它是地址为 `symbol:Name` 的链接(见 symbolHref)。
*/
export interface SymbolLink extends SymbolReference {
path?: undefined
@@ -118,31 +119,34 @@ export function codeLocationHref(code: string): string | undefined {
}
/**
* 页面上一个链接元素是哪种能点的链接(同 parseCommentLink)。不带路径的引用再带上它写在哪块的注释里:离它最近的那张块卡片(.cr-block)
* 上记着的块记号(data-anchor);不在块卡片里的、那张卡片没记块记号的(删掉的块)不带,扩展端只从文件顶层找。
* 页面上一个链接元素是哪种能点的链接(同 parseCommentLink)。不带路径的引用再带上它写在哪块、哪份文件的注释里:
* 块是离它最近的那张块卡片(.cr-block)上记着的块记号(data-anchor),不在块卡片里的、那张卡片没记块记号的(删掉的块)不带,扩展端只从文件顶层找;
* 文件是它所在的定义卡片浮窗根上记着的那份文件([PEEK_ATTRIBUTE]),页面上这份文档自己的注释不带。
*/
export function commentLinkOf(element: Element): CommentLink | undefined {
const link = parseCommentLink(element.getAttribute('href') ?? '')
if (!link || link.path !== undefined)
return link
const block = element.closest<HTMLElement>('.cr-block')?.dataset.anchor
return block === undefined ? link : { ...link, block }
const file = element.closest(`[${PEEK_ATTRIBUTE}]`)?.getAttribute(PEEK_ATTRIBUTE) ?? undefined
return { ...link, ...(block !== undefined && { block }), ...(file !== undefined && { file }) }
}
function symbolKey(path: string, symbol: string): string {
return `${path}#${symbol}`
}
/** 扩展端报回来打没打开的一个链接(repoLinkBroken / repoLinkOpened),path、symbol、block 同 openRepoLink。 */
/** 扩展端报回来打没打开的一个链接(repoLinkBroken / repoLinkOpened),path、symbol、block、file 同 openRepoLink。 */
interface ReportedLink {
path?: string
symbol?: string
block?: string
file?: string
}
/** 不带路径的引用的键:同一个名字写在不同块的注释里分开记(指到的可以不一样)。 */
function referenceKey({ symbol, block }: SymbolReference): string {
return `${symbol}\u0000${block ?? ''}`
/** 不带路径的引用的键:同一个名字写在不同块、不同文件的注释里分开记(指到的可以不一样)。 */
function referenceKey({ symbol, block, file }: SymbolReference): string {
return `${symbol}\u0000${block ?? ''}\u0000${file ?? ''}`
}
/** 页面上一个链接眼下怎么显示:照常的链接;坏的(文件、符号没有,引用找不到);普通文字(语言自带、又问不到定义的名字,点了不打开)。 */
@@ -150,7 +154,7 @@ export type LinkState = 'link' | 'broken' | 'plain'
/**
* 页面上的仓库链接和不带路径的引用:点了发给扩展端打开;预览出来后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;
* 扩展端说没有的标成坏的,说是语言自带的当普通文字。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪块,名字)分开问、分开标。
* 扩展端说没有的标成坏的,说是语言自带的当普通文字。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪份文件、哪块,名字)分开问、分开标。
*/
export class RepoLinks {
private request = 0
@@ -174,7 +178,7 @@ export class RepoLinks {
return true
}
/** 页面上现有的链接。路径、引用(名字或写在哪块)变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */
/** 页面上现有的链接。路径、引用(名字或写在哪份文件、哪块)变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */
scan(links: readonly CommentLink[]): void {
const paths = [...new Set(links.flatMap(link => link.path === undefined ? [] : [link.path]))].sort()
const references = new Map<string, SymbolReference>()
@@ -208,12 +212,12 @@ export class RepoLinks {
return true
}
/** 扩展端打开时发现文件或符号没有;没有路径的是写在 block 那块里的不带路径的引用找不到。 */
broken({ path, symbol, block }: ReportedLink): void {
/** 扩展端打开时发现文件或符号没有;没有路径的是写在 file 那份文件 block 那块里的不带路径的引用找不到。 */
broken({ path, symbol, block, file }: ReportedLink): void {
if (path === undefined) {
if (symbol !== undefined) {
this.unresolved.add(referenceKey({ symbol, block }))
this.builtins.delete(referenceKey({ symbol, block }))
this.unresolved.add(referenceKey({ symbol, block, file }))
this.builtins.delete(referenceKey({ symbol, block, file }))
}
return
}
@@ -223,12 +227,12 @@ export class RepoLinks {
this.missingPaths.add(path)
}
/** 打开成功:这个路径在,这个符号也在;没有路径的是写在 block 那块里的不带路径的引用找到了。 */
opened({ path, symbol, block }: ReportedLink): void {
/** 打开成功:这个路径在,这个符号也在;没有路径的是写在 file 那份文件 block 那块里的不带路径的引用找到了。 */
opened({ path, symbol, block, file }: ReportedLink): void {
if (path === undefined) {
if (symbol !== undefined) {
this.unresolved.delete(referenceKey({ symbol, block }))
this.builtins.delete(referenceKey({ symbol, block }))
this.unresolved.delete(referenceKey({ symbol, block, file }))
this.builtins.delete(referenceKey({ symbol, block, file }))
}
return
}
@@ -237,13 +241,13 @@ export class RepoLinks {
this.missingSymbols.delete(symbolKey(path, symbol))
}
/** 扩展端打开时发现写在 `block` 那块里的不带路径的引用是语言自带、又问不到定义的名字:改当普通文字,不再标坏。 */
/** 扩展端打开时发现写在 `file` 那份文件 `block` 那块里的不带路径的引用是语言自带、又问不到定义的名字:改当普通文字,不再标坏。 */
builtin(reference: SymbolReference): void {
this.unresolved.delete(referenceKey(reference))
this.builtins.add(referenceKey(reference))
}
/** 这个链接现在怎么显示(见 [LinkState]):文件没有、这个符号没有、写在这块里的引用找不到是坏的,写在这块里的引用是语言自带的名字是普通文字。还没问到的照常是链接。 */
/** 这个链接现在怎么显示(见 [LinkState]):文件没有、这个符号没有、引用(按写在哪份文件、哪块分开算)找不到是坏的,引用是语言自带的名字是普通文字。还没问到的照常是链接。 */
state(link: CommentLink | undefined): LinkState {
if (!link)
return 'link'
+16 -12
View File
@@ -199,11 +199,13 @@ export interface TranslatorOption { id: string, label: string }
/**
* 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor),
* 扩展端从那块代码的声明起一层层往外找。不在哪块的注释里的没有 block,只从文件顶层找。
* 同一个名字写在不同块的注释里,指到的可以不一样。
* file 是这段注释写在哪份文件里(uri,见 [DefinitionPeek] 的 `uri`):定义卡片里的注释带着卡片那份文件,扩展端按那份文件的块、导入找;
* 页面上这份文档自己的注释没有 file。同一个名字写在不同块、不同文件的注释里,指到的可以不一样。
*/
export interface SymbolReference {
symbol: string
block?: string
file?: string
}
/** 这份文件里出现过的一个属性值:显示名和有几块是这个值 */
@@ -251,12 +253,14 @@ export interface Occurrence {
}
/**
* 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(从工作区文件夹算起的相对路径)、定义处那一行(0 起),
* 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(path 是给人看的、从工作区文件夹算起的相对路径;
* uri 是这份文件本身,卡片里注释中不带路径的引用带着它,扩展端按这份文件找,见 [SymbolReference])、定义处那一行(0 起),
* 盖住定义处的那块(连同子块,和那个文件的预览画的是同一张卡片;它自己当那个文件的顶层第一块,span 从文件开头算),
* 显示成中文名的名字(下标是那个文件里的,不是页面上这份文档的)
*/
export interface DefinitionPeek {
path: string
uri: string
line: number
block: BlockView
names: ShownName[]
@@ -539,9 +543,9 @@ export interface CodeColors {
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用,
* builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`),当普通文字显示;两样都原样带回问的那几条
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
* block 同 openRepoLink
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,改当普通文字显示;symbol、block 同 openRepoLink
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
* block、file 同 openRepoLink
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,改当普通文字显示;symbol、block、file 同 openRepoLink
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block、file 同 openRepoLink
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
* callTreeCounts:version 这份文档里,每个能看调用树的块落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
* callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。
@@ -609,9 +613,9 @@ export type ExtensionToWebview
| { type: 'diff', diff: DocumentDiff | null }
| { type: 'openedFrom', from: OpenedFrom }
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] }
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string, file?: string }
| ({ type: 'repoLinkBuiltin' } & SymbolReference)
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string, file?: string }
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
| { type: 'focusPathAsk' }
| { type: 'callTreeCounts', version: number, counts: Record<string, number> }
@@ -625,7 +629,7 @@ export type ExtensionToWebview
* version 是这份下标所属的文档版本
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
* peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block、file 同 openRepoLink
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
* setFilter:改块筛选(整份替换);不在本地先改,等扩展端发回 filter 再按它显示
* setOnlyChanges:打开 / 关掉「只看改动」;同样等扩展端发回 filter 再按它显示
@@ -643,8 +647,8 @@ export type ExtensionToWebview
* findNavigation:眼下查找栏的快捷键(下一处 / 上一处)归不归这个页面(查找栏开着、页面有焦点),变了才发
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,lines 是文件里的几行(和 symbol 二选一),
* 都没有就只打开文件或在代码阅读侧栏的树上定位目录;
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference),
* 从那块的声明起一层层往外找
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按它写在的那份文件找 symbol:file 是那份文件(定义卡片里的注释才有,没有是这份文件),
* block 是它写在哪块的注释里(见 SymbolReference),从那块的声明起一层层往外找
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
* openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本
@@ -664,7 +668,7 @@ export type WebviewToExtension
| { type: 'probeDefinition', id: number, version: number, offset: number }
| { type: 'goToDefinition', version: number, offset: number }
| { type: 'peekDefinition', id: number, version: number, offset: number }
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string }
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string, file?: string }
| { type: 'findOccurrences', id: number, version: number, offset: number }
| { type: 'ask', askId: string, anchor: string, question: string, images: AskImage[] }
| { type: 'stopAsk', askId: string }
@@ -672,7 +676,7 @@ export type WebviewToExtension
| { type: 'selectProfile', kind: ChatKind, id: string }
| { type: 'occurrenceNavigation', available: boolean }
| { type: 'findNavigation', available: boolean }
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string, lines?: LineRange }
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string, file?: string, lines?: LineRange }
| { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] }
| { type: 'checkRepoImages', id: number, paths: string[] }
| { type: 'openCallTree', version: number, anchor: string }
@@ -43,7 +43,7 @@ function paint(links: RepoLinks): void {
/**
* 注释、说明、问 AI 的回答里的仓库路径链接,和注释里不带路径的引用:点了发给扩展端打开(不走 webview 把 http 链接交给外部打开的那条),
* 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;文档换了版本再问一次(引用跟着源码变)。
* 不带路径的引用都带着它写在哪块(见 commentLinkOf),扩展端从那块的声明起找。
* 不带路径的引用都带着它写在哪块(定义卡片里的还带着卡片那份文件,见 commentLinkOf),扩展端按那份文件、从那块的声明起找。
*/
export function useRepoLinks(): void {
const [links] = useState(() => new RepoLinks(postMessage))