🐛 fix(vscode): 代码阅读按 ⌘↓ 跳高亮不再来回闪,筛选栏加上上一处、下一处

This commit is contained in:
cruldra committed 2026-10-03 03:11:46 +08:00
1 parent 70a53e5a68
commit b16cc6409e
8 files changed
+221 -36

No files matched your search

@@ -1,7 +1,7 @@
import type { Click, Highlight, ScrollPosition } from './occurrences'
import type { BlockView, Occurrence } from './types'
import { createContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { clickAction, KeyScroll, OccurrenceHighlight, OccurrenceNavigation } from './occurrences'
import { createContext, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { clickAction, KeyScroll, OccurrenceHighlight, occurrenceJumpKey, OccurrenceNavigation } from './occurrences'
import { occurrencePlan } from './reveal'
import { showInViewport } from './useReveal'
import { onMessage, postMessage } from './vscode'
@@ -52,29 +52,41 @@ function scrollPosition(): ScrollPosition {
return { x: window.scrollX, y: window.scrollY }
}
/** mac 用 ⌘,别的系统用 ctrl。页面跑在 VS Code 的网页里,平台从浏览器读。 */
function isMac(): boolean {
return /Mac|iPhone|iPad/.test(navigator.platform)
}
/**
* 立刻滚到这里,顺带打断浏览器正在播(或刚排上、还没开始播)的滚动动画。滚到原地浏览器当没滚、不打断动画,
* 所以先挪一个像素再回来:同一段脚本里连着滚两下,中间不画,画面上看不出。
* 这两处位置先交给 own:滚动事件可能晚于这次调用才到,不能被当成浏览器把页面滚走。
*/
function scrollBack({ x, y }: ScrollPosition): void {
window.scrollTo({ left: x, top: y > 0 ? y - 1 : y + 1, behavior: 'instant' })
function scrollBack({ x, y }: ScrollPosition, own: (position: ScrollPosition) => void): void {
const nudged = { x, y: y > 0 ? y - 1 : y + 1 }
own(nudged)
own({ x, y })
window.scrollTo({ left: x, top: nudged.y, behavior: 'instant' })
window.scrollTo({ left: x, top: y, behavior: 'instant' })
}
/**
* 单击名字高亮它在文档里的各处出现,点空白处、Esc、换文档版本或翻译清掉;返回眼下高亮着的名字。
* 高亮着时扩展端的「跳到下一处 / 上一处高亮」命令发来 moveOccurrence:当前那一处换成下一处页面上找得到的出现,
* 展开把它折叠住的块、属性行和子行,再把它滚进视口。
* 触发命令的那个键浏览器自己的滚动不拦(页面不知道是哪个键),跳的时候盖掉它,见 KeyScroll。
* 单击名字高亮它在文档里的各处出现,点空白处、Esc、换文档版本或翻译清掉;返回眼下高亮着的名字,和筛选栏上「上一处 / 下一处」用的跳转。
* 高亮着时扩展端的「跳到下一处 / 上一处高亮」命令发来 moveOccurrence,筛选栏上的按钮走同一条跳转:当前那一处换成下一处页面上找得到的出现,
* 展开把它折叠住的块、属性行和子行,再把它滚进视口。按钮不是按键,不按 ⌘↓ 那样拉回按键时的位置。
* 默认的 ⌘↓ / ⌘↑(Windows 是 ctrl)在能跳的时候取消浏览器滚到底 / 滚到顶,但不停事件继续传(VS Code 在后面才把按键收成命令)。
* 别的键浏览器仍可能自己滚,跳的时候盖掉它,见 KeyScroll。
*/
export function useOccurrences(version: number | undefined, translatorId: string | undefined, page: OccurrencePage | undefined): Highlight | undefined {
export function useOccurrences(version: number | undefined, translatorId: string | undefined, page: OccurrencePage | undefined): { highlight: Highlight | undefined, moveOccurrence: (step: 1 | -1) => void } {
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)
/** 筛选栏按钮调用的跳转。真正的函数在下面的效果里,挂上监听时才有。 */
const moveFromButton = useRef<(step: 1 | -1) => void>(() => {})
useLayoutEffect(() => {
latestPage.current = page
@@ -91,16 +103,22 @@ export function useOccurrences(version: number | undefined, translatorId: string
return
const el = document.querySelector<HTMLElement>('[data-current]')
if (el)
showInViewport(el)
showInViewport(el, position => keyScroll.own(position))
keyScroll.hold(scrollPosition())
}, [moves, keyScroll])
useEffect(() => {
const move = (step: 1 | -1): void => {
// 触发命令的那个键浏览器可能已经照自己的意思滚了(⌘↓ 滚到底),先回到按键那一刻
const before = keyScroll.take(performance.now())
if (before)
scrollBack(before)
const move = (step: 1 | -1, fromKey: boolean): void => {
if (fromKey) {
// 触发命令的那个键浏览器可能已经照自己的意思滚了(⌘↓ 滚到底),先回到按键那一刻
const before = keyScroll.take(performance.now())
if (before)
scrollBack(before, position => keyScroll.own(position))
}
else {
// 筛选栏按钮不是按键,别把这次滚动当成 ⌘↓ 拉回按键时的位置
keyScroll.forget()
}
const current = latestPage.current
const planOf = (occurrence: Occurrence): ReturnType<typeof occurrencePlan> => current && occurrencePlan(current.blocks, occurrence, current.skip)
const target = current && occurrences.move(step, occurrence => planOf(occurrence) !== undefined)
@@ -116,13 +134,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)
moveFromButton.current = step => move(step, false)
// 任何键都照常交给 VS Code。能跳时,默认的 ⌘↓ / ⌘↑(Windows 是 ctrl)取消浏览器滚到底 / 滚到顶,但不停冒泡,命令还是收得到
const onKeyDown = (event: KeyboardEvent): void => {
if (navigation.available && occurrenceJumpKey(event, isMac()))
event.preventDefault()
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)
scrollBack(back, position => keyScroll.own(position))
}
const onKey = (event: KeyboardEvent): void => {
// 输入框、下拉列表自己用掉的 Esc 不算
@@ -137,7 +160,7 @@ export function useOccurrences(version: number | undefined, translatorId: string
if (msg.type === 'occurrencesFound')
occurrences.answer(msg.id, msg.occurrences)
else if (msg.type === 'moveOccurrence')
move(msg.step)
move(msg.step, true)
})
navigation.update({})
document.addEventListener('click', onClick, true)
@@ -165,5 +188,6 @@ export function useOccurrences(version: number | undefined, translatorId: string
}
}, [occurrences, navigation, keyScroll])
return highlight
const moveOccurrence = useCallback((step: 1 | -1) => moveFromButton.current(step), [])
return { highlight, moveOccurrence }
}