♻️ refactor(vscode): 代码阅读不再在页面里拦 ⌘↓ / ⌘↑,快捷键只在「键盘快捷方式」里绑;跳到下一处时把浏览器自己的滚动拉回来,页面不再先滚到底

This commit is contained in:
cruldra committed 2026-09-27 00:33:18 +08:00
1 parent 0cad5722bb
commit 1b1299dc1f
10 files changed
+157 -165

No files matched your search

@@ -1,7 +1,7 @@
import type { Click, Highlight } from './occurrences'
import type { BlockView, KeyCombo, Occurrence } from './types'
import type { Click, Highlight, ScrollPosition } from './occurrences'
import type { BlockView, Occurrence } from './types'
import { createContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { clickAction, OccurrenceHighlight, OccurrenceNavigation } from './occurrences'
import { clickAction, KeyScroll, OccurrenceHighlight, OccurrenceNavigation } from './occurrences'
import { occurrencePlan } from './reveal'
import { showInViewport } from './useReveal'
import { onMessage, postMessage } from './vscode'
@@ -47,15 +47,30 @@ function isTyping(target: EventTarget | null): boolean {
|| (target instanceof HTMLInputElement && !NON_TEXT_INPUTS.has(target.type))
}
function scrollPosition(): ScrollPosition {
return { x: window.scrollX, y: window.scrollY }
}
/**
* 立刻滚到这里,顺带打断浏览器正在播(或刚排上、还没开始播)的滚动动画。滚到原地浏览器当没滚、不打断动画,
* 所以先挪一个像素再回来:同一段脚本里连着滚两下,中间不画,画面上看不出。
*/
function scrollBack({ x, y }: ScrollPosition): void {
window.scrollTo({ left: x, top: y > 0 ? y - 1 : y + 1, behavior: 'instant' })
window.scrollTo({ left: x, top: y, behavior: 'instant' })
}
/**
* 单击名字高亮它在文档里的各处出现,点空白处、Esc、换文档版本或翻译清掉;返回眼下高亮着的名字。
* 高亮着时扩展端的「跳到下一处 / 上一处高亮」命令发来 moveOccurrence:当前那一处换成下一处页面上找得到的出现,
* 展开把它折叠住的块、属性行和子行,再把它滚进视口。
* 触发命令的那个键浏览器自己的滚动不拦(页面不知道是哪个键),跳的时候盖掉它,见 KeyScroll。
*/
export function useOccurrences(version: number | undefined, translatorId: string | undefined, page: OccurrencePage | undefined): Highlight | undefined {
const [highlight, setHighlight] = useState<Highlight | undefined>()
const [occurrences] = useState(() => new OccurrenceHighlight(postMessage, setHighlight))
const [navigation] = useState(() => new OccurrenceNavigation(postMessage, { focused: document.hasFocus(), typing: isTyping(document.activeElement) }))
const [keyScroll] = useState(() => new KeyScroll())
/** 用键盘跳过几次:每跳一次,画完后把当前那一处滚进视口 */
const [moves, setMoves] = useState(0)
const latestPage = useRef(page)
@@ -71,22 +86,28 @@ export function useOccurrences(version: number | undefined, translatorId: string
// 要展开的已经展开、当前那一处已经画出来(data-current),页面上按先后第一个就是它
useLayoutEffect(() => {
const el = moves > 0 ? document.querySelector<HTMLElement>('[data-current]') : null
if (moves === 0)
return
const el = document.querySelector<HTMLElement>('[data-current]')
if (el)
showInViewport(el)
}, [moves])
keyScroll.hold(scrollPosition())
}, [moves, keyScroll])
useEffect(() => {
let keys: KeyCombo[] = []
const move = (step: 1 | -1): void => {
// 触发命令的那个键浏览器可能已经照自己的意思滚了(⌘↓ 滚到底),先回到按键那一刻
const before = keyScroll.take(performance.now())
if (before)
scrollBack(before)
const current = latestPage.current
if (!current)
return
const planOf = (occurrence: Occurrence): ReturnType<typeof occurrencePlan> => occurrencePlan(current.blocks, occurrence, current.skip)
const target = occurrences.move(step, occurrence => planOf(occurrence) !== undefined)
const planOf = (occurrence: Occurrence): ReturnType<typeof occurrencePlan> => current && occurrencePlan(current.blocks, occurrence, current.skip)
const target = current && occurrences.move(step, occurrence => planOf(occurrence) !== undefined)
const plan = target && planOf(target)
if (!plan)
if (!current || !plan) {
keyScroll.hold(scrollPosition())
return
}
const folded = plan.expand.filter(id => current.collapsed.has(id))
if (folded.length > 0)
current.expand(folded)
@@ -94,12 +115,18 @@ export function useOccurrences(version: number | undefined, translatorId: string
}
// 捕获阶段:赶在块里的按钮处理点击、折叠箭头换图标之前读出点在哪
const onClick = (event: MouseEvent): void => occurrences.click(clickAction(clickOf(event)))
// 任何键都照常交给浏览器和 VS Code,这里只记下按键那一刻页面滚在哪
const onKeyDown = (event: KeyboardEvent): void => keyScroll.press(scrollPosition(), event.timeStamp)
const onPointer = (): void => keyScroll.forget()
const onScroll = (event: Event): void => {
const back = keyScroll.scrolled(scrollPosition(), event.timeStamp)
if (back)
scrollBack(back)
}
const onKey = (event: KeyboardEvent): void => {
// 输入框、下拉列表自己用掉的 Esc 不算
if (event.key === 'Escape' && !event.defaultPrevented)
occurrences.clear()
else if (navigation.blocks(event, keys))
event.preventDefault()
}
const onFocus = (): void => navigation.update({ focused: true })
const onBlur = (): void => navigation.update({ focused: false })
@@ -108,13 +135,15 @@ export function useOccurrences(version: number | undefined, translatorId: string
const off = onMessage((msg) => {
if (msg.type === 'occurrencesFound')
occurrences.answer(msg.id, msg.occurrences)
else if (msg.type === 'occurrenceKeys')
keys = msg.keys
else if (msg.type === 'moveOccurrence')
move(msg.step)
})
navigation.update({})
document.addEventListener('click', onClick, true)
window.addEventListener('keydown', onKeyDown, true)
window.addEventListener('pointerdown', onPointer, true)
window.addEventListener('wheel', onPointer, { capture: true, passive: true })
window.addEventListener('scroll', onScroll, { passive: true })
window.addEventListener('keydown', onKey)
window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur)
@@ -123,13 +152,17 @@ export function useOccurrences(version: number | undefined, translatorId: string
return () => {
off()
document.removeEventListener('click', onClick, true)
window.removeEventListener('keydown', onKeyDown, true)
window.removeEventListener('pointerdown', onPointer, true)
window.removeEventListener('wheel', onPointer, { capture: true })
window.removeEventListener('scroll', onScroll)
window.removeEventListener('keydown', onKey)
window.removeEventListener('focus', onFocus)
window.removeEventListener('blur', onBlur)
document.removeEventListener('focusin', onFocusIn)
document.removeEventListener('focusout', onFocusOut)
}
}, [occurrences, navigation])
}, [occurrences, navigation, keyScroll])
return highlight
}