import type { LinkState } from './repoLink' import { useEffect, useState } from 'react' import { commentLinkOf, RepoLinks } from './repoLink' import { onMessage, postMessage } from './vscode' /** 坏链接悬停上的字,走页面上统一的 data-tooltip,不用原生 title。 */ const MISSING = '找不到' /** * 按链接眼下的状态(见 [LinkState])画:文件或符号没有的仓库链接、找不到的不带路径的引用是红色波浪线,悬停「找不到」; * 语言自带的名字画成普通文字。问到之前先是普通链接。 */ function paintLink(anchor: HTMLAnchorElement, state: LinkState): void { anchor.classList.toggle('cr-link-plain', state === 'plain') const tip = anchor.getAttribute('data-tooltip') if (state === 'broken') { anchor.classList.add('cr-link-broken') if (tip !== MISSING) { if (tip) anchor.dataset.linkTooltip = tip anchor.setAttribute('data-tooltip', MISSING) } return } if (!anchor.classList.contains('cr-link-broken')) return anchor.classList.remove('cr-link-broken') if (tip === MISSING) { const prev = anchor.dataset.linkTooltip if (prev) anchor.setAttribute('data-tooltip', prev) else anchor.removeAttribute('data-tooltip') } delete anchor.dataset.linkTooltip } function paint(links: RepoLinks): void { for (const anchor of document.querySelectorAll('.cr-markdown a')) paintLink(anchor, links.state(commentLinkOf(anchor))) } /** * 注释、说明、问 AI 的回答里的仓库路径链接,和注释里不带路径的引用:点了发给扩展端打开(不走 webview 把 http 链接交给外部打开的那条), * 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;文档换了版本再问一次(引用跟着源码变)。 * 不带路径的引用都带着它写在哪块(定义卡片里的还带着卡片那份文件,见 commentLinkOf),扩展端按那份文件、从那块的声明起找。 */ export function useRepoLinks(): void { const [links] = useState(() => new RepoLinks(postMessage)) useEffect(() => { let timer = 0 const scan = (): void => { window.clearTimeout(timer) timer = window.setTimeout(() => { const found = [...document.querySelectorAll('.cr-markdown a')].map(anchor => ({ anchor, link: commentLinkOf(anchor) })) for (const { anchor, link } of found) paintLink(anchor, links.state(link)) links.scan(found.flatMap(({ link }) => link ?? [])) }, 50) } const off = onMessage((msg) => { if (msg.type === 'document') { links.stale() scan() return } if (msg.type === 'repoLinksChecked') { 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) return } if (msg.type === 'repoLinkOpened') { links.opened(msg) paint(links) } }) // 捕获阶段:赶在 webview 把 交给外部打开之前拦下仓库链接 const onClick = (event: MouseEvent): void => { const anchor = event.target instanceof Element ? event.target.closest('a') : null if (!anchor || !links.click(commentLinkOf(anchor))) return event.preventDefault() event.stopPropagation() } const observer = new MutationObserver(scan) observer.observe(document.body, { childList: true, subtree: true }) document.addEventListener('click', onClick, true) document.addEventListener('auxclick', onClick, true) scan() return () => { off() observer.disconnect() document.removeEventListener('click', onClick, true) document.removeEventListener('auxclick', onClick, true) window.clearTimeout(timer) } }, [links]) }