import React, { useEffect, useRef, useCallback, useState } from 'react' import { useTabStore } from '../../stores/tabStore' import { useEditorStore } from '../../stores/editorStore' import { useSearchStore } from '../../stores/searchStore' import { findMatches, buildLineStarts, getLineNumber } from '../../lib/searchEngine' import type { SearchMatch } from '../../types/search' export function Editor() { const textareaRef = useRef(null) const lineNumbersRef = useRef(null) const wrapperRef = useRef(null) const activeTabId = useTabStore(s => s.activeTabId) const getActiveTab = useTabStore(s => s.getActiveTab) const updateTabContent = useTabStore(s => s.updateTabContent) const setModified = useTabStore(s => s.setModified) const searchStore = useSearchStore() const [lineCount, setLineCount] = useState(1) const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 }) const [fileSize, setFileSize] = useState('') const updateTimerRef = useRef | null>(null) // 测量行高 const lineHeightRef = useRef(20.8) useEffect(() => { const textarea = textareaRef.current if (!textarea) return const style = getComputedStyle(textarea) const measure = document.createElement('div') measure.style.cssText = `position:absolute;visibility:hidden;font-family:${style.fontFamily};font-size:${style.fontSize};line-height:${style.lineHeight};white-space:pre;width:0;` measure.textContent = 'X' document.body.appendChild(measure) const singleHeight = measure.offsetHeight measure.textContent = 'X\nX' const doubleHeight = measure.offsetHeight document.body.removeChild(measure) lineHeightRef.current = doubleHeight - singleHeight || 20.8 }, []) // 加载标签内容到编辑器 useEffect(() => { const tab = getActiveTab() const textarea = textareaRef.current if (!tab || !textarea) return textarea.value = tab.content updateLineNumbers(tab.content) updateFileSizeDisplay(tab.content) updateCursorPos() requestAnimationFrame(() => { textarea.scrollTop = tab.scrollTop textarea.scrollLeft = tab.scrollLeft textarea.setSelectionRange(tab.selectionStart, tab.selectionEnd) if (lineNumbersRef.current) { lineNumbersRef.current.scrollTop = tab.scrollTop } }) }, [activeTabId, getActiveTab]) // 保存当前标签状态 const saveCurrentState = useCallback(() => { const tab = getActiveTab() const textarea = textareaRef.current if (!tab || !textarea) return useTabStore.getState().updateTabScroll(tab.id, { scrollTop: textarea.scrollTop, scrollLeft: textarea.scrollLeft, selectionStart: textarea.selectionStart, selectionEnd: textarea.selectionEnd, previewScrollTop: 0 // will be set by preview }) }, [getActiveTab]) // 更新行号 const updateLineNumbers = useCallback((content: string) => { const count = (content.match(/\n/g) || []).length + 1 setLineCount(count) }, []) // 更新文件大小 const updateFileSizeDisplay = useCallback((content: string) => { const bytes = new Blob([content]).size if (bytes < 1024) setFileSize(bytes + ' B') else if (bytes < 1024 * 1024) setFileSize((bytes / 1024).toFixed(1) + ' KB') else setFileSize((bytes / (1024 * 1024)).toFixed(1) + ' MB') }, []) // 更新光标位置 const updateCursorPos = useCallback(() => { const textarea = textareaRef.current if (!textarea) return const text = textarea.value const pos = textarea.selectionStart const textBefore = text.substring(0, pos) const lines = textBefore.split('\n') setCursorPos({ line: lines.length, col: lines[lines.length - 1].length + 1 }) }, []) // 输入事件 const handleInput = useCallback(() => { const textarea = textareaRef.current const tab = getActiveTab() if (!textarea || !tab) return const content = textarea.value updateTabContent(tab.id, content) setModified(tab.id, true) updateLineNumbers(content) updateFileSizeDisplay(content) // 防抖更新 if (updateTimerRef.current) clearTimeout(updateTimerRef.current) updateTimerRef.current = setTimeout(() => { // 触发预览更新(通过 store) }, 150) }, [getActiveTab, updateTabContent, setModified, updateLineNumbers, updateFileSizeDisplay]) // 滚动同步 const handleScroll = useCallback(() => { const textarea = textareaRef.current if (!textarea) return if (lineNumbersRef.current) { lineNumbersRef.current.scrollTop = textarea.scrollTop } }, []) // Tab 键支持 const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Tab') { e.preventDefault() const textarea = textareaRef.current if (!textarea) return const start = textarea.selectionStart const end = textarea.selectionEnd if (start !== end) { const lines = textarea.value.substring(start, end).split('\n') const indented = lines.map(line => ' ' + line).join('\n') document.execCommand('insertText', false, indented) } else { document.execCommand('insertText', false, ' ') } } // 搜索导航 if (e.key === 'Enter' && searchStore.isVisible) { e.preventDefault() if (e.shiftKey) { searchStore.findPrev() } else { searchStore.findNext() } } }, [searchStore]) // 渲染行号 const renderLineNumbers = () => { const lineHeight = lineHeightRef.current const LARGE_THRESHOLD = 2000 if (lineCount > LARGE_THRESHOLD) { // 虚拟化 const textarea = textareaRef.current if (!textarea) return null const scrollTop = textarea.scrollTop const viewportHeight = textarea.clientHeight const buffer = 20 const startLine = Math.max(0, Math.floor(scrollTop / lineHeight) - buffer) const endLine = Math.min(lineCount, Math.ceil((scrollTop + viewportHeight) / lineHeight) + buffer) const topPadding = startLine * lineHeight const bottomPadding = (lineCount - endLine) * lineHeight return ( <>
{Array.from({ length: endLine - startLine }, (_, i) => (
{startLine + i + 1}
))}
) } return Array.from({ length: lineCount }, (_, i) => (
{i + 1}
)) } return (
{renderLineNumbers()}