import { useEffect, useState } from 'react' import { DefinitionLinks } from './definitionLinks' import { onMessage, postMessage } from './vscode' /** 跳到定义用的修饰键,按平台习惯:macOS 是 ⌘,其余是 Ctrl。 */ const IS_MAC = /Mac|iPhone|iPad/.test(navigator.userAgent) function modifierHeld(event: MouseEvent | KeyboardEvent): boolean { return IS_MAC ? event.metaKey : event.ctrlKey } /** 事件落在的那个带源码下标的名字(SegmentText 渲染的 data-offset)。 */ function nameAt(target: EventTarget | null): HTMLElement | null { return target instanceof Element ? target.closest('[data-offset]') : null } function offsetOf(el: HTMLElement | null): number | undefined { return el?.dataset.offset === undefined ? undefined : Number(el.dataset.offset) } /** 指针下的名字、修饰键按没按;把链接样式(data-link)挂到指着的那个名字上。 */ class Pointer { private hovered: HTMLElement | null = null private held = false private linked: HTMLElement | null = null constructor(private readonly links: DefinitionLinks) {} move(target: EventTarget | null, held: boolean): void { this.hovered = nameAt(target) this.held = held this.sync() } hold(held: boolean): void { this.held = held this.sync() } /** 把按住修饰键时指着的名字告诉 links,再按它的回答挂上或摘掉链接样式 */ sync(): void { const offset = offsetOf(this.hovered) this.links.point(this.held ? offset : undefined) const link = offset !== undefined && offset === this.links.linked ? this.hovered : null if (link === this.linked) return this.linked?.removeAttribute('data-link') link?.setAttribute('data-link', '') this.linked = link } } /** * 和 VS Code 编辑器里一样:按住 ⌘ / Ctrl、鼠标停在查得到定义的名字上时,名字显示成链接,点一下跳到定义。 * version 是当前显示的文档版本,名字的下标属于它。 */ export function useDefinitionLinks(version: number | undefined): void { const [links] = useState(() => new DefinitionLinks(postMessage)) const [pointer] = useState(() => new Pointer(links)) useEffect(() => { links.setVersion(version) pointer.sync() }, [links, pointer, version]) useEffect(() => { const onMove = (event: MouseEvent): void => pointer.move(event.target, modifierHeld(event)) const onOut = (event: MouseEvent): void => { if (event.relatedTarget === null) pointer.move(null, false) } const onKey = (event: KeyboardEvent): void => pointer.hold(modifierHeld(event)) const onBlur = (): void => pointer.hold(false) // ⌘ / Ctrl + 点击名字归跳转:拦下这次点击,不让它再触发块上的编辑切换、折叠 const onClick = (event: MouseEvent): void => { if (!links.click(modifierHeld(event) ? offsetOf(nameAt(event.target)) : undefined)) return event.preventDefault() event.stopPropagation() } const off = onMessage((msg) => { if (msg.type !== 'definitionProbed') return links.answer(msg.id, msg.found) pointer.sync() }) document.addEventListener('mousemove', onMove) document.addEventListener('mouseout', onOut) // 捕获阶段:先于块里的按钮、折叠箭头拿到点击 document.addEventListener('click', onClick, true) window.addEventListener('keydown', onKey) window.addEventListener('keyup', onKey) window.addEventListener('blur', onBlur) return () => { off() document.removeEventListener('mousemove', onMove) document.removeEventListener('mouseout', onOut) document.removeEventListener('click', onClick, true) window.removeEventListener('keydown', onKey) window.removeEventListener('keyup', onKey) window.removeEventListener('blur', onBlur) } }, [links, pointer]) }