✨ feat(vscode): 代码阅读预览界面
Vite 加 code-reader.html 第二入口;代码块树(层级竖线、折叠、种类标签配色)、译文 / 源码按词性着色、块内直接编辑(防抖回写整行、Esc 回预览)、翻译下拉、解析失败提示;看板与代码阅读共用带类型的 acquireVsCodeApi 包装和 ErrorBoundary。
This commit is contained in:
1 parent
cab5ac0ebc
commit
af56985d9a
16 files changed
+775
-74
No files matched your search
@@ -0,0 +1,129 @@
|
||||
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react'
|
||||
import type { BlockView } from './types'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { recolor } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { postMessage } from './vscode'
|
||||
|
||||
const EDIT_DEBOUNCE_MS = 200
|
||||
|
||||
interface CodeEditorProps {
|
||||
block: BlockView
|
||||
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一块的代码没变 */
|
||||
version: number
|
||||
autoFocus: boolean
|
||||
onAutoFocused: () => void
|
||||
onExit: () => void
|
||||
}
|
||||
|
||||
/** 一块源码的编辑状态:着色层显示,透明输入框接收输入,改动防抖后整行替换回文件。 */
|
||||
export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
|
||||
const [text, setText] = useState(block.code)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const blockRef = useRef(block)
|
||||
blockRef.current = block
|
||||
const textRef = useRef(text)
|
||||
textRef.current = text
|
||||
/** 本地改过、还没在新文档里看到的文字;有它时新文档不能覆盖有焦点的输入框 */
|
||||
const unconfirmed = useRef<string | null>(null)
|
||||
/** 最近发出的文字在文件里占几行:新文档到之前,下一次替换要盖住这么多行,而不是旧文档里的行数 */
|
||||
const sentLineCount = useRef<number | null>(null)
|
||||
const timer = useRef<number | undefined>(undefined)
|
||||
const unsent = useRef(false)
|
||||
const restoreSelection = useRef<[number, number] | null>(null)
|
||||
|
||||
function flush(): void {
|
||||
window.clearTimeout(timer.current)
|
||||
if (!unsent.current)
|
||||
return
|
||||
unsent.current = false
|
||||
const { startLine, endLine } = blockRef.current
|
||||
const value = textRef.current
|
||||
postMessage({
|
||||
type: 'edit',
|
||||
startLine,
|
||||
endLine: sentLineCount.current === null ? endLine : startLine + sentLineCount.current - 1,
|
||||
text: value,
|
||||
})
|
||||
sentLineCount.current = value.split('\n').length
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
const focused = el !== null && document.activeElement === el
|
||||
if (focused && unconfirmed.current !== null && unconfirmed.current !== block.code)
|
||||
return
|
||||
unconfirmed.current = null
|
||||
sentLineCount.current = null
|
||||
if (block.code === textRef.current)
|
||||
return
|
||||
if (focused)
|
||||
restoreSelection.current = [el.selectionStart, el.selectionEnd]
|
||||
setText(block.code)
|
||||
}, [version, block.code])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = textareaRef.current
|
||||
const selection = restoreSelection.current
|
||||
if (!el || !selection)
|
||||
return
|
||||
restoreSelection.current = null
|
||||
el.setSelectionRange(Math.min(selection[0], text.length), Math.min(selection[1], text.length))
|
||||
}, [text])
|
||||
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
if (!autoFocus || !el)
|
||||
return
|
||||
el.focus()
|
||||
el.setSelectionRange(el.value.length, el.value.length)
|
||||
onAutoFocused()
|
||||
}, [autoFocus, onAutoFocused])
|
||||
|
||||
// 卸载(切回预览、块消失)前把没发出去的改动发掉
|
||||
useEffect(() => flush, [])
|
||||
|
||||
function onChange(e: ChangeEvent<HTMLTextAreaElement>): void {
|
||||
const value = e.target.value
|
||||
setText(value)
|
||||
unconfirmed.current = value
|
||||
unsent.current = true
|
||||
window.clearTimeout(timer.current)
|
||||
timer.current = window.setTimeout(flush, EDIT_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>): void {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
flush()
|
||||
onExit()
|
||||
}
|
||||
// ⌘S 由 VS Code 接管保存:先把还在防抖里的改动发出去,免得存的是旧内容
|
||||
else if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cr-editor">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="cr-code"
|
||||
value={text}
|
||||
rows={1}
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
aria-label="源码"
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={flush}
|
||||
/>
|
||||
<pre className="cr-code" aria-hidden="true">
|
||||
<SegmentText segments={recolor(block.codeTokens, block.code, text)} />
|
||||
{text === '' || text.endsWith('\n') ? ' ' : ''}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user