✨ feat(vscode): 代码阅读预览加 ⌘F 查找栏:整份文件查(折叠着的块里的也算,跳过去时展开),按页面上显示的字不分大小写匹配、中文名也认原名,Enter / ⌘G 跳、Esc 关,高亮直接画在页面上不重画卡片
This commit is contained in:
1 parent
cc3fcda051
commit
bd474986bd
27 files changed
+1833
-114
No files matched your search
@@ -0,0 +1,210 @@
|
||||
import type { NameMap } from './displayNames'
|
||||
import type { FindAnchor, FindMatch, FindResult, FindUnit } from './find'
|
||||
import type { FindPainter } from './findHighlights'
|
||||
import type { TextMarks } from './SegmentText'
|
||||
import type { ReadableStore } from './store'
|
||||
import type { OccurrencePage } from './useOccurrences'
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { findCount, findUnits, PageFind } from './find'
|
||||
import { markdownText, textOf } from './findHighlights'
|
||||
import { ShortcutContext } from './occurrences'
|
||||
import { pageLayout } from './pageLayout'
|
||||
import { useStore } from './store'
|
||||
import { showInViewport } from './useReveal'
|
||||
import { onMessage, postMessage } from './vscode'
|
||||
|
||||
/** 打字停下多久才找:大文件里每敲一个字都整份找一遍、重画一遍高亮会卡 */
|
||||
const TYPING_MS = 100
|
||||
|
||||
const NO_UNITS: readonly FindUnit[] = []
|
||||
const NO_NAMES: NameMap = new Map()
|
||||
const NOTHING: FindResult = { query: '', matches: [] }
|
||||
|
||||
/** 查找栏要画的和它的动作,见 [FindBar](vscode/webview-ui/src/codeReader/FindBar.tsx#FindBar)。 */
|
||||
export interface FindView {
|
||||
open: boolean
|
||||
query: string
|
||||
count?: string
|
||||
canMove: boolean
|
||||
focus: number
|
||||
onQuery: (query: string) => void
|
||||
onMove: (step: 1 | -1) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/** 一次找:查的字,从哪往后找(⌘F 拿选中的字填进去时是选中的那处)。 */
|
||||
interface Search {
|
||||
query: string
|
||||
from?: FindAnchor
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面上选中的一行字,和查找从哪开始(它在页面内容 `root` 里哪段字的第几个字);没选中、选中了好几行、焦点在输入框里时没有。
|
||||
* `line` 是那块在文件里的第一行(见 [FindAnchor])。
|
||||
*/
|
||||
function selectedText(root: HTMLElement | null, page: OccurrencePage | undefined): Search | undefined {
|
||||
const selection = window.getSelection()
|
||||
const query = selection?.toString() ?? ''
|
||||
const field = document.activeElement
|
||||
if (!selection || selection.rangeCount === 0 || query.trim() === '' || /[\r\n]/.test(query) || field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)
|
||||
return undefined
|
||||
const { startContainer, startOffset } = selection.getRangeAt(0)
|
||||
const holder = (startContainer instanceof Element ? startContainer : startContainer.parentElement)?.closest<HTMLElement>('[data-find]')
|
||||
const node = holder && root?.contains(holder) ? textOf(holder).nodes.find(item => item.node === startContainer) : undefined
|
||||
const block = holder?.closest<HTMLElement>('.cr-block')?.dataset.id
|
||||
if (!holder || !node || block === undefined)
|
||||
return { query }
|
||||
return { query, from: { key: holder.dataset.find, start: node.start + startOffset, line: page?.placement.of(block)?.line ?? 0 } }
|
||||
}
|
||||
|
||||
/** 视口顶部那块(钉住的筛选栏下面)的行:查找栏刚写字、还没有当前那处时从它往后找。 */
|
||||
function viewportAnchor(): FindAnchor {
|
||||
return { start: 0, line: pageLayout.anchorAt(window.scrollY + pageLayout.pinnedHeight())?.line ?? 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找栏:扩展端的「查找」命令(⌘F)发来 openFind 时打开、聚焦输入框,页面上选中了一行字时先填进去、从它那处开始找;
|
||||
* 写了字停下一会儿就在整份文件里找(见 [PageFind]),当前那处展开把它折叠住的块、属性行和子行,滚进视口;
|
||||
* 「查找下一处 / 上一处」命令(⌘G / ⇧⌘G)发来 moveFind、输入框里 Enter / Shift+Enter、栏上的按钮都在命中之间跳。
|
||||
* 高亮由 `painter` 画在页面上,块树不重画;关掉时清掉高亮,查的字留着,下次打开照旧。
|
||||
* 查找栏开着、页面有焦点时告诉扩展端 ⌘G 归这个页面(见 [ShortcutContext])。
|
||||
* `page` 同单击高亮跳到一处出现用的页面状态,`marks` 是页面文字的标记(中文名在里面,找的时候才订阅,中文名到了不连累整页重画)。
|
||||
*/
|
||||
export function useFind(page: OccurrencePage | undefined, marks: ReadableStore<TextMarks>, painter: FindPainter): FindView {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [search, setSearch] = useState<Search>({ query: '' })
|
||||
/** 每次 ⌘F 加一:聚焦输入框、选中里面的字 */
|
||||
const [focus, setFocus] = useState(0)
|
||||
const [result, setResult] = useState(NOTHING)
|
||||
const [find] = useState(() => new PageFind(setResult))
|
||||
const [shortcuts] = useState(() => new ShortcutContext(
|
||||
{ open: false, focused: document.hasFocus() },
|
||||
facts => facts.open && facts.focused,
|
||||
available => postMessage({ type: 'findNavigation', available }),
|
||||
))
|
||||
/** 跳过几次:每跳一次,画完后把当前那处滚进视口 */
|
||||
const [reveals, setReveals] = useState(0)
|
||||
const latest = useRef({ page, open, query, search })
|
||||
|
||||
useLayoutEffect(() => {
|
||||
latest.current = { page, open, query, search }
|
||||
})
|
||||
|
||||
const reachable = useCallback((unit: FindUnit) => latest.current.page?.skip(unit.block) === false, [])
|
||||
|
||||
/** 跳到 match:先展开把它折叠住的,画完后滚过去 */
|
||||
const reveal = useCallback((match: FindMatch) => {
|
||||
const current = latest.current.page
|
||||
const folded = current ? find.unitOf(match).expand.filter(id => current.collapsed.has(id)) : []
|
||||
if (current && folded.length > 0)
|
||||
current.expand(folded)
|
||||
setReveals(count => count + 1)
|
||||
}, [find])
|
||||
|
||||
// 开着、写了字才把整份文件的字备好;关着时不跟着新文档重算,原来那处留着,下次打开从它附近接着
|
||||
const active = open && search.query !== ''
|
||||
const names = useStore(marks, current => active ? current.names : NO_NAMES)
|
||||
const blocks = page?.blocks
|
||||
const placement = page?.placement
|
||||
const units = useMemo(() => active && blocks && placement ? findUnits(blocks, placement, names, markdownText) : NO_UNITS, [active, blocks, placement, names])
|
||||
|
||||
// 先换上新的字(新一版文档、中文名到了、又打开了)再找:当前那处停在原来那处附近,不滚
|
||||
useLayoutEffect(() => {
|
||||
if (active)
|
||||
find.setUnits(units, reachable)
|
||||
}, [find, active, units, reachable])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const from = search.from ?? (search.query !== '' && find.state.current === undefined ? viewportAnchor() : undefined)
|
||||
const match = find.search(search.query, reachable, from)
|
||||
if (match)
|
||||
reveal(match)
|
||||
}, [find, search, reachable, reveal])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => setSearch(previous => previous.query === query ? previous : { query }), TYPING_MS)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [query])
|
||||
|
||||
// 结果变了、开关了:照着画;之后页面变了(展开、换了一版文档)由 painter 自己下一帧重画
|
||||
useLayoutEffect(() => {
|
||||
painter.show(open && result.query !== '' ? find.paint() : undefined)
|
||||
}, [painter, find, open, result])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (reveals === 0)
|
||||
return
|
||||
// 要展开的已经展开:按展开后的页面重画一遍,再滚到当前那处;它画不出来就滚到它那段字、那块
|
||||
const painted = painter.show(latest.current.open ? find.paint() : undefined)
|
||||
const { current, matches } = find.state
|
||||
const block = current === undefined ? undefined : find.unitOf(matches[current]).block
|
||||
const target = painted.current ?? painted.text ?? (block === undefined ? null : document.querySelector<HTMLElement>(`.cr-block[data-id="${block}"]`))
|
||||
if (target)
|
||||
showInViewport(target)
|
||||
painter.place()
|
||||
}, [reveals, painter, find])
|
||||
|
||||
const move = useCallback((step: 1 | -1) => {
|
||||
const { open, query, search } = latest.current
|
||||
if (!open)
|
||||
return
|
||||
// 刚写的字还没找:先找,跳到它的第一处
|
||||
if (query !== search.query) {
|
||||
setSearch({ query })
|
||||
return
|
||||
}
|
||||
const match = find.move(step, reachable)
|
||||
if (match)
|
||||
reveal(match)
|
||||
}, [find, reachable, reveal])
|
||||
|
||||
const close = useCallback(() => {
|
||||
const focused = document.activeElement
|
||||
if (focused instanceof HTMLElement && focused.closest('.cr-find-bar'))
|
||||
focused.blur()
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => onMessage((msg) => {
|
||||
if (msg.type === 'openFind') {
|
||||
const selected = selectedText(painter.root, latest.current.page)
|
||||
if (selected) {
|
||||
setQuery(selected.query)
|
||||
setSearch(selected)
|
||||
}
|
||||
setOpen(true)
|
||||
setFocus(count => count + 1)
|
||||
}
|
||||
else if (msg.type === 'moveFind') {
|
||||
move(msg.step)
|
||||
}
|
||||
}), [painter, move])
|
||||
|
||||
useEffect(() => shortcuts.update({ open }), [shortcuts, open])
|
||||
|
||||
useEffect(() => {
|
||||
const onFocus = (): void => shortcuts.update({ focused: true })
|
||||
const onBlur = (): void => shortcuts.update({ focused: false })
|
||||
window.addEventListener('focus', onFocus)
|
||||
window.addEventListener('blur', onBlur)
|
||||
// 焦点落进页面里(⌘F 聚焦输入框)时页面一定有焦点
|
||||
document.addEventListener('focusin', onFocus)
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
document.removeEventListener('focusin', onFocus)
|
||||
}
|
||||
}, [shortcuts])
|
||||
|
||||
return {
|
||||
open,
|
||||
query,
|
||||
count: open ? findCount(result) : undefined,
|
||||
canMove: result.matches.length > 0,
|
||||
focus,
|
||||
onQuery: setQuery,
|
||||
onMove: move,
|
||||
onClose: close,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user