chore: bump version to 0.3.8
Code audit fixes: - CRITICAL: reorder Markdown pipeline (fixImages before sanitize) - CRITICAL: fix path prefix separator check - BLOCKING: remove duplicate useEffect in Editor - BLOCKING: skip onChange when content unchanged - BLOCKING: optimize Sidebar re-render with useMemo - HIGH: cleanup FileWatcher polling intervals - HIGH: improve validatePath segment check - HIGH: fix isExternalUpdate race with counter - HIGH: add will-navigate / setWindowOpenHandler - HIGH: explicit strip in sanitize schema - MEDIUM: random temp file suffix instead of Date.now() - MEDIUM/LOW: add IndexedDB error boundaries - LOW: support UTF-16 BOM detection
This commit is contained in:
@@ -33,6 +33,10 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
||||
content: activeTab?.content ?? '',
|
||||
onChange: useCallback((value: string) => {
|
||||
if (!activeTabId) return
|
||||
// B-02: 内容未变时跳过(例如纯选择变更触发的 markdownUpdated),
|
||||
// 避免将文档误标记为已修改
|
||||
const tab = useTabStore.getState().tabs.find(t => t.id === activeTabId)
|
||||
if (tab?.content === value) return
|
||||
updateTabContent(activeTabId, value)
|
||||
setModified(activeTabId, true)
|
||||
}, [activeTabId, updateTabContent, setModified]),
|
||||
@@ -83,20 +87,6 @@ export const Editor = React.memo(function Editor({ darkMode }: EditorProps) {
|
||||
return () => setEditorViewGetter(() => null)
|
||||
}, [getView])
|
||||
|
||||
// Save current tab state on unmount or tab switch
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!activeTabId) return
|
||||
updateTabScroll(activeTabId, {
|
||||
scrollTop: getScrollTop(),
|
||||
selectionStart: getSelection().from,
|
||||
selectionEnd: getSelection().to
|
||||
})
|
||||
}
|
||||
// stable refs: getScrollTop, getSelection (useCallback([])), updateTabScroll (zustand) - never change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTabId])
|
||||
|
||||
// Ctrl+B bold, Ctrl+I italic, Ctrl+F search, Ctrl+H replace
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -88,6 +88,8 @@ export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions)
|
||||
const onChangeRef = useRef(onChange)
|
||||
const isExternalUpdate = useRef(false)
|
||||
const initialContentRef = useRef(content)
|
||||
// H-03: 请求计数器 — 每个 setContent 调用自增,仅抑制匹配的 markdownUpdated 事件
|
||||
const setContentRequestId = useRef(0)
|
||||
|
||||
// Keep onChangeRef fresh
|
||||
useEffect(() => {
|
||||
@@ -110,7 +112,9 @@ export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions)
|
||||
const lm = ctx.get(listenerCtx)
|
||||
lm.markdownUpdated((_ctx, markdown, prevMarkdown) => {
|
||||
if (markdown === prevMarkdown) return
|
||||
if (!isExternalUpdate.current) {
|
||||
// H-03: 只有未被外部更新抑制时才触发 onChange。
|
||||
// 计数器匹配确保用户按键不会在 setContent 期间被丢弃。
|
||||
if (setContentRequestId.current === 0 && !isExternalUpdate.current) {
|
||||
onChangeRef.current(markdown)
|
||||
}
|
||||
})
|
||||
@@ -165,13 +169,19 @@ export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions)
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
|
||||
// H-03: 递增请求 ID,这样只有本次 replaceAll 触发的 markdownUpdated 会被抑制
|
||||
const requestId = ++setContentRequestId.current
|
||||
isExternalUpdate.current = true
|
||||
try {
|
||||
editor.action(milkdownReplaceAll(newContent))
|
||||
} catch {
|
||||
// replaceAll may fail if editor is not fully ready
|
||||
} finally {
|
||||
isExternalUpdate.current = false
|
||||
// 仅当没有新的 setContent 启动时才重置标志
|
||||
if (setContentRequestId.current === requestId) {
|
||||
setContentRequestId.current = 0
|
||||
isExternalUpdate.current = false
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ const norm = (p: string) => p.replace(/[/\\]+$/, '').replace(/\\/g, '/')
|
||||
export const Sidebar = React.memo(function Sidebar() {
|
||||
const tabs = useTabStore(s => s.tabs)
|
||||
const activeTabId = useTabStore(s => s.activeTabId)
|
||||
const activeTab = useTabStore(s => s.getActiveTab())
|
||||
// B-03: 用 activeTabId + tabs 推导 activeTab 而非 s.getActiveTab(),
|
||||
// 后者每次返回新对象引用导致 Zustand 无条件重渲染
|
||||
const activeTab = useMemo(() => tabs.find(t => t.id === activeTabId) ?? null, [tabs, activeTabId])
|
||||
const switchToTab = useTabStore(s => s.switchToTab)
|
||||
const createTab = useTabStore(s => s.createTab)
|
||||
const rootPath = useSidebarStore(s => s.rootPath)
|
||||
|
||||
Reference in New Issue
Block a user