Files
superwork/vscode/webview-ui/src/codeReader/CodeEditor.tsx
T
cruldra 6ce4ef7ee2 ✨ feat(vscode): React 文件照源码一块一张卡片,组件卡片只留名称、说明、构造参数
- 组件体的语句不再并进组件卡片,照源码顺序是它的子块;状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出各成一张卡片,回调和函数里的语句是这些卡片下面的子卡片
- 普通变量、提前返回的 if、注释照语言默认的卡片;表格格子里只写名字和位置,去掉「改它的」「来自」格子里单独打开源码的那条路
- 用词改成书面叫法(更新位置、使用位置、计算表达式、依赖项、触发时机、清理函数、绑定事件、样式取决于……),新增 toggle-right、anchor、download、sigma、layout-template 五个种类图标
- 夹具加上 luffy 的 QueuedBubble.tsx、PlanChecklist.tsx,规格的 React 一节照新样子改写
2026-10-08 07:17:25 +08:00

130 lines
4.2 KiB
TypeScript

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>
)
}