From 4c875ceff71689bb902b92f5f65379c0f5651576 Mon Sep 17 00:00:00 2001 From: MXY <393121069@qq.com> Date: Wed, 19 Aug 2026 04:04:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E9=A2=84=E8=A7=88=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E4=B8=89=E4=BB=B6=E5=A5=97=E2=80=94=E2=80=94=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E5=88=B7=E6=96=B0=E6=8C=89=E9=92=AE=20+=20=E5=88=87?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BD=BD=20+=20?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E4=BF=9D=E5=AD=98=E5=90=8E=E9=87=8D=E8=BD=BD?= =?UTF-8?q?=EF=BC=88fixes=20#167=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A. header 新增刷新按钮(toolbar 非空时显示),经 reloadSeq 重跑加载 effect;B. 编辑→预览切换时自动重载(dirty 或保存失败则抑制,避免丢草稿);C. 预览模式下 saveState 进入 saved 边沿触发一次重载。 新增 tests/editor-refresh.spec.tsx 4 用例(A 重载计数 / B 正常与 dirty 抑制 / C saved 边沿);全量 622 通过(其余 8 失败为既有 Windows 平台问题:pty-deps/side-card 的 AttachConsole)。 --- .../2026-08-19-editor-preview-refresh.md | 27 +++ src/client/EditorHost.tsx | 42 +++- tests/editor-refresh.spec.tsx | 188 ++++++++++++++++++ 3 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 docs/plans/2026-08-19-editor-preview-refresh.md create mode 100644 tests/editor-refresh.spec.tsx diff --git a/docs/plans/2026-08-19-editor-preview-refresh.md b/docs/plans/2026-08-19-editor-preview-refresh.md new file mode 100644 index 0000000..9cae7a3 --- /dev/null +++ b/docs/plans/2026-08-19-editor-preview-refresh.md @@ -0,0 +1,27 @@ +# 编辑器预览刷新三件套(issue #167) + +日期:2026-08-19 + +## 问题(#167,无评论无修复) + +`.md` 文件编辑保存后,切到预览不立即渲染新内容,需关闭文件重新打开。根因:`EditorHost` 的加载 effect 只依赖 `[sessionId, cwd, path, ctx, showEmpty]`——保存(fsWrite)与模式切换(setMode)都不重跑加载,且工具栏没有手动刷新入口。 + +## 方案(最小三件套,全部在 EditorHost.tsx) + +| 编号 | 行为 | 实现 | +|------|------|------| +| A | 手动刷新按钮(文本类 viewer,即 toolbar 非空时显示) | 新增 `reloadSeq` state;effect 依赖加 `reloadSeq`;header 刷新按钮 `setReloadSeq(s => s + 1)` | +| B | 编辑保存后切回预览自动刷新 | header「预览」按钮 onClick:`toolbar.mode === 'edit'` 且非 dirty 且 saveState 非 failed → 先 `setReloadSeq(+1)` 再 `setMode('preview')`;dirty 时不刷新(草稿只在 CodeMirror 内,重载会丢) | +| C | 预览模式下保存成功后刷新 | 监听 `toolbar.saveState` 从非 `'saved'` 变为 `'saved'` 且 `mode === 'preview'` → `setReloadSeq(+1)` | + +刷新语义:重载走既有 load effect(AbortController 会中止旧加载;重挂载后 TextEditor 内部 state 重置为 preview 模式——与「关闭重开」等价,但保留 tab 与面板状态)。 + +## 范围外(记录,不实现) + +- LLM 改文件后自动刷新(磁盘指纹对比 + silent 不闪屏)——分形有先例,另行立项; +- image/pdf/binary viewer 的手动刷新(toolbar 为空,无入口;内容静态,低优先级)。 + +## 验证 + +- 单测:刷新按钮渲染 + 点击触发重载(mock fsRead 调用次数);B 的 dirty 抑制;C 的 saveState 边沿触发; +- 手工:编辑 md → 保存 → 切预览即时渲染;预览模式 Ctrl+S 后刷新;刷新按钮任意 viewer 可用。 diff --git a/src/client/EditorHost.tsx b/src/client/EditorHost.tsx index 984a1d7..635ca4e 100644 --- a/src/client/EditorHost.tsx +++ b/src/client/EditorHost.tsx @@ -25,7 +25,7 @@ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react' import { createElement } from 'react' import clsx from 'clsx' -import { IconCheckOutline16, IconFolderOpen16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconCheckOutline16, IconFolderOpen16, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context } from '../context-types.ts' import { api, mediaUrl, type SessionScope } from './api.ts' import { BinaryDownload } from './binary-download.tsx' @@ -97,6 +97,9 @@ export function EditorHost(props: { const path = tab.path ?? '' const title = tab.title const [load, setLoad] = useState({ status: 'loading' }) + // Manual refresh (issue #167): bumping the sequence re-runs the load effect + // with the same path/scope — the only reload entry besides open/close. + const [reloadSeq, setReloadSeq] = useState(0) // Reactive prefs read: flipping editorExplorer re-renders this tab with no // reload. The snapshot is the bare boolean so unrelated store churn never @@ -247,7 +250,20 @@ export function EditorHost(props: { } apply(planFirstMatch(ctx.betterSidebar?.matchFileViewer(path), mediaUrlOf)) return () => { cancelled = true; controller.abort() } - }, [scope.sessionId, scope.cwd, path, ctx, showEmpty]) + }, [scope.sessionId, scope.cwd, path, ctx, showEmpty, reloadSeq]) + + // Save-then-refresh in preview mode (issue #167 part C): the edge into + // 'saved' (never a lingering 'saved' state) triggers exactly one reload, so + // a preview-mode Ctrl+S shows the fresh content immediately. Edit mode is + // left alone — reloading would remount the editor and drop the caret. + const prevSaveState = useRef(undefined) + useEffect(() => { + const current = toolbar?.saveState + if (prevSaveState.current !== 'saved' && current === 'saved' && toolbar?.mode === 'preview') { + setReloadSeq(sequence => sequence + 1) + } + prevSaveState.current = current + }, [toolbar?.saveState, toolbar?.mode]) const treeOpen = treeOpenOf(tab) /** Persist the panel flag on the tab (survives reloads with the layout). */ @@ -287,7 +303,16 @@ export function EditorHost(props: { @@ -315,6 +340,17 @@ export function EditorHost(props: { {saveLabel !== '' && ( {saveLabel} )} + {toolbar !== null && ( + + )}