Files
superwork/vscode/webview-ui/src/codeReader/useRepoLinks.ts
T

110 lines
4.0 KiB
TypeScript

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<HTMLAnchorElement>('.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<HTMLAnchorElement>('.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 把 <a> 交给外部打开之前拦下仓库链接
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])
}