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(null) const blockRef = useRef(block) blockRef.current = block const textRef = useRef(text) textRef.current = text /** 本地改过、还没在新文档里看到的文字;有它时新文档不能覆盖有焦点的输入框 */ const unconfirmed = useRef(null) /** 最近发出的文字在文件里占几行:新文档到之前,下一次替换要盖住这么多行,而不是旧文档里的行数 */ const sentLineCount = useRef(null) const timer = useRef(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): 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): void { if (e.key === 'Escape') { e.preventDefault() flush() onExit() } // ⌘S 由 VS Code 接管保存:先把还在防抖里的改动发出去,免得存的是旧内容 else if (e.key === 's' && (e.metaKey || e.ctrlKey)) { flush() } } return (