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

105 lines
3.8 KiB
TypeScript

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<HTMLElement>('[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])
}