Skip to content

Commit 6fc105b

Browse files
authored
Merge pull request #16 from davidhoo/codex/release-v2.3.1-rollback
release: v2.3.1 functional rollback
2 parents e44317a + 717fde2 commit 6fc105b

10 files changed

Lines changed: 125 additions & 268 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@
1414
- 菜单 nil target 禁用状态、PDF sheet 附着等需真实焦点环境的验证尚未覆盖(SwiftUI Commands 焦点读取无法用普通 XCTest 可靠覆盖),待补最小 UI harness
1515
- 双窗口/多目录/最小化/全屏/关闭最后窗口重开等人工回归矩阵未执行,需 GUI 环境验证
1616

17+
## [2.3.1] - 2026-08-17
18+
19+
### 修复
20+
21+
- **回退编辑模式分栏预览**:撤回 v2.3.0 引入的 Raw 编辑左右分栏实时预览、默认分栏设置及末尾滚动改动,恢复到 v2.2.10 的稳定编辑与渲染行为。v2.3.1 保持更高版本号,确保已升级到 v2.3.0 的用户可通过应用内更新恢复稳定版本。
22+
1723
## [2.3.0] - 2026-08-17
1824

25+
> 此版本因编辑模式分栏预览相关问题已撤回;请使用 v2.3.1。
26+
1927
### 新增
2028

2129
- **编辑模式左右分栏实时预览**:Raw 编辑模式可在标题栏用「切换分栏预览」按钮开关左右分栏,左栏编辑原文、右栏同步显示渲染结果,不再需要切换到渲染模式才能看到效果。预览在停止输入约 200 ms 后更新,连续快速输入不会触发反复重渲染

Sources/MarkdownReader/Models/SettingsModel.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ final class SettingsModel {
4545

4646
private enum Keys {
4747
static let defaultDisplayMode = "com.markdownreader.defaultDisplayMode"
48-
static let defaultSplitPreview = "com.markdownreader.defaultSplitPreview"
4948
static let reopenLastLocation = "com.markdownreader.reopenLastLocation"
5049
static let showHiddenFiles = "com.markdownreader.showHiddenFiles"
5150
static let showNonMarkdownFiles = "com.markdownreader.showNonMarkdownFiles"
@@ -89,11 +88,6 @@ final class SettingsModel {
8988
didSet { defaults.set(defaultDisplayMode.rawValue, forKey: Keys.defaultDisplayMode) }
9089
}
9190

92-
/// 进入编辑模式时默认开启左右分栏预览
93-
var defaultSplitPreview: Bool {
94-
didSet { defaults.set(defaultSplitPreview, forKey: Keys.defaultSplitPreview) }
95-
}
96-
9791
/// 启动时重新打开上次位置
9892
var reopenLastLocation: Bool {
9993
didSet { defaults.set(reopenLastLocation, forKey: Keys.reopenLastLocation) }
@@ -388,7 +382,6 @@ final class SettingsModel {
388382
let defaults = UserDefaults.standard
389383

390384
self.defaultDisplayMode = DisplayMode(rawValue: defaults.string(forKey: Keys.defaultDisplayMode) ?? "") ?? .rendered
391-
self.defaultSplitPreview = defaults.object(forKey: Keys.defaultSplitPreview) as? Bool ?? false
392385
self.languagePref = LanguagePref(rawValue: defaults.string(forKey: Keys.languagePref) ?? "") ?? .auto
393386
self.reopenLastLocation = defaults.object(forKey: Keys.reopenLastLocation) as? Bool ?? false
394387
self.showHiddenFiles = defaults.object(forKey: Keys.showHiddenFiles) as? Bool ?? false

Sources/MarkdownReader/ViewModels/AppViewModel.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ final class AppViewModel {
6464
/// 大纲侧边栏是否可见
6565
var isOutlineVisible: Bool = false
6666

67-
/// 编辑模式是否启用左右分栏实时预览(默认关闭,由标题栏按钮切换)
68-
var isSplitPreviewEnabled: Bool = false
69-
7067
/// 大纲侧边栏宽度
7168
var outlineWidth: CGFloat = 200
7269

@@ -135,13 +132,6 @@ final class AppViewModel {
135132
}
136133
}
137134

138-
/// 切换编辑模式分栏预览
139-
func toggleSplitPreview() {
140-
withAnimation(.spring(duration: 0.25)) {
141-
isSplitPreviewEnabled.toggle()
142-
}
143-
}
144-
145135
/// 切换查找面板显隐
146136
func toggleFindBar(expandReplace: Bool = false) {
147137
withAnimation(.easeOut(duration: 0.2)) {

Sources/MarkdownReader/ViewModels/WindowSession.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,6 @@ final class WindowSession {
8787
self.appViewModel = AppViewModel()
8888
self.fileTreeViewModel = FileTreeViewModel(settings: settings)
8989
self.documentViewModel = DocumentViewModel(settings: settings)
90-
// 默认显示模式为「编辑」时,按「默认分栏」设置初始化分栏预览。
91-
// 视图层通过 onChange(displayMode) 应用该设置,但 init 阶段赋的初始值不会触发 onChange,需在此显式同步。
92-
if settings.defaultDisplayMode == .raw {
93-
self.appViewModel.isSplitPreviewEnabled = settings.defaultSplitPreview
94-
}
9590
self.commandPaletteViewModel = CommandPaletteViewModel()
9691
self.identityService = identityService
9792
self.coordinator = coordinator

Sources/MarkdownReader/Views/DetailView.swift

Lines changed: 79 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,6 @@ struct DetailView: View {
7575
/// 导出用的 WebPage 引用
7676
@State private var exportedPage: WebPage?
7777

78-
/// 编辑模式右侧实时预览的内容(防抖更新,避免每次击键都全量渲染)
79-
@State private var previewContent: String = ""
80-
81-
/// 分栏预览 WebView 自身的 WebPage 引用。
82-
/// 不与渲染模式的 `exportedPage` 共享:预览 WebView 移除/重建时不应影响渲染模式
83-
/// 仍持有的 WebPage,避免两者在切模式/切分栏的过渡窗口争抢同一绑定导致 use-after-free。
84-
/// Raw 模式 PDF 导出走独立 HTML 路径,不读取此绑定。
85-
@State private var previewExportedPage: WebPage?
86-
8778

8879
var body: some View {
8980
VStack(spacing: 0) {
@@ -240,19 +231,6 @@ struct DetailView: View {
240231
}
241232
// 操作按钮组与大纲图标下对齐,横向间隔一致
242233
HStack(alignment: .bottom, spacing: 8) {
243-
// 分栏预览切换(仅编辑模式且非纯文本时显示)
244-
if documentViewModel.hasDocument && documentViewModel.displayMode == .raw && !documentViewModel.isPlainTextMode {
245-
Button {
246-
appViewModel.toggleSplitPreview()
247-
} label: {
248-
Image(systemName: "rectangle.split.2x1")
249-
.font(.system(size: 14))
250-
.foregroundStyle(appViewModel.isSplitPreviewEnabled ? themeColors.accent : themeColors.fgMuted)
251-
}
252-
.buttonStyle(.plain)
253-
.help(L10n.tr(.titleBarToggleSplitPreview, language: language))
254-
}
255-
256234
// 刷新按钮(文件被外部修改时显示,在保存按钮左侧)
257235
if documentViewModel.hasDocument && documentViewModel.isFileModifiedExternally {
258236
Button {
@@ -625,98 +603,74 @@ struct DetailView: View {
625603

626604
@ViewBuilder
627605
private var documentContentView: some View {
628-
HStack(spacing: 0) {
629-
ZStack {
630-
// Raw 模式视图 — 始终保持存活,避免 NSTextView 被销毁导致 undo 历史丢失
631-
RawMarkdownView(
632-
content: Binding(
633-
get: { documentViewModel.content },
634-
set: { documentViewModel.content = $0 }
635-
),
636-
fontSize: settings.sourceFontPointSize,
606+
ZStack {
607+
// Raw 模式视图 — 始终保持存活,避免 NSTextView 被销毁导致 undo 历史丢失
608+
RawMarkdownView(
609+
content: Binding(
610+
get: { documentViewModel.content },
611+
set: { documentViewModel.content = $0 }
612+
),
613+
fontSize: settings.sourceFontPointSize,
614+
contentPadding: settings.contentPaddingPoints,
615+
scrollToLine: documentViewModel.scrollToLineRequest,
616+
fileURL: documentViewModel.currentFileURL,
617+
isActive: documentViewModel.displayMode == .raw,
618+
isFindBarVisible: appViewModel.isFindBarVisible,
619+
searchRef: textViewSearchRef,
620+
onCursorLineNumberChanged: { lineNumber in
621+
documentViewModel.cursorLineNumber = lineNumber
622+
},
623+
contentVersion: documentViewModel.contentVersion,
624+
undoStore: undoStore
625+
)
626+
.opacity(documentViewModel.displayMode == .raw ? 1 : 0)
627+
.allowsHitTesting(documentViewModel.displayMode == .raw)
628+
.onChange(of: documentViewModel.scrollToLineRequest) { _, newValue in
629+
if newValue != nil {
630+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
631+
documentViewModel.clearScrollRequest()
632+
}
633+
}
634+
}
635+
636+
// 渲染模式视图 — 仅在渲染模式下显示
637+
if documentViewModel.displayMode == .rendered {
638+
WebViewMarkdownView(
639+
content: documentViewModel.content,
640+
fileURL: documentViewModel.currentFileURL,
637641
contentPadding: settings.contentPaddingPoints,
642+
maxContentWidthFollowsWindow: settings.maxContentWidthFollowsWindow,
638643
scrollToLine: documentViewModel.scrollToLineRequest,
639-
fileURL: documentViewModel.currentFileURL,
640-
isActive: documentViewModel.displayMode == .raw,
644+
themeCSS: themeColors.cssCustomProperties + themeColors.codeHighlightCSS,
645+
isDark: settings.resolvedThemeType == .dark,
646+
documentCopyEnabled: settings.enableDocumentCopy,
647+
searchQuery: findReplaceViewModel.searchText,
648+
searchCaseSensitive: findReplaceViewModel.isCaseSensitive,
649+
searchWholeWord: findReplaceViewModel.isWholeWord,
650+
searchCurrentIndex: findReplaceViewModel.currentMatchIndex,
641651
isFindBarVisible: appViewModel.isFindBarVisible,
642-
searchRef: textViewSearchRef,
643-
onCursorLineNumberChanged: { lineNumber in
644-
documentViewModel.cursorLineNumber = lineNumber
645-
},
646652
contentVersion: documentViewModel.contentVersion,
647-
undoStore: undoStore
653+
onVisibleHeadingChanged: { heading in
654+
activeOutlineLineNumber = heading?.lineNumber
655+
},
656+
onVisibleLineChanged: { lineNumber in
657+
documentViewModel.renderedVisibleLineNumber = lineNumber
658+
},
659+
commandTarget: commandTarget,
660+
onOpenLinkedMarkdownFile: { [weak session] url in
661+
session?.handleLinkedMarkdownFile(url.standardizedFileURL)
662+
},
663+
exportedPage: $exportedPage
648664
)
649-
.opacity(documentViewModel.displayMode == .raw ? 1 : 0)
650-
.allowsHitTesting(documentViewModel.displayMode == .raw)
651665
.onChange(of: documentViewModel.scrollToLineRequest) { _, newValue in
652666
if newValue != nil {
653-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
667+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
654668
documentViewModel.clearScrollRequest()
655669
}
656670
}
657671
}
658-
659-
// 渲染模式视图 — 仅在渲染模式下显示(渲染模式布局保持不变)
660-
if documentViewModel.displayMode == .rendered {
661-
webMarkdownView(content: documentViewModel.content, exportedPage: $exportedPage)
662-
}
663-
}
664-
.frame(maxWidth: .infinity, maxHeight: .infinity)
665-
.overlay(alignment: .topTrailing) {
666-
// 编辑模式内容区右上角原生复制按钮。
667-
// 仅 raw 模式 + 有文档 + 查找栏关闭 + 内容一键复制总开关开启时显示,
668-
// 避免与查找栏浮层重叠,并遵从总开关。
669-
// 挂在左侧源码容器上:分栏时按钮保留在 Markdown 源码栏,右侧预览栏不显示。
670-
if documentViewModel.hasDocument
671-
&& documentViewModel.displayMode == .raw
672-
&& !appViewModel.isFindBarVisible
673-
&& settings.enableDocumentCopy {
674-
Button {
675-
copyRawContent()
676-
} label: {
677-
Image(systemName: contentCopyState.isShowingSuccess ? DocumentCopySymbol.copied.rawValue : DocumentCopySymbol.copy.rawValue)
678-
.font(.system(size: 11))
679-
.frame(width: 14, height: 14)
680-
.foregroundStyle(contentCopyState.isShowingSuccess ? themeColors.success : themeColors.fgMuted)
681-
.frame(width: 24, height: 24)
682-
}
683-
.buttonStyle(.plain)
684-
.help(L10n.tr(contentCopyState.isShowingSuccess ? .contentCopied : .contentCopy, language: language))
685-
.padding(.trailing, 11)
686-
.padding(.top, 3)
687-
.transition(.opacity)
688-
}
689-
}
690-
691-
// 编辑模式:右侧实时渲染预览(默认关闭,经标题栏按钮开启;纯文本模式不支持)
692-
if documentViewModel.displayMode == .raw && !documentViewModel.isPlainTextMode && appViewModel.isSplitPreviewEnabled {
693-
Rectangle().fill(themeColors.border).frame(width: 1)
694-
695-
webMarkdownView(content: previewContent, exportedPage: $previewExportedPage)
696-
.frame(maxWidth: .infinity, maxHeight: .infinity)
697672
}
698673
}
699-
.onAppear { previewContent = documentViewModel.content }
700-
.onChange(of: documentViewModel.displayMode) { _, mode in
701-
// 进入编辑模式时立即同步一次,避免右栏短暂空白
702-
if mode == .raw {
703-
previewContent = documentViewModel.content
704-
// 应用「默认分栏」设置:进入编辑模式时按设置决定是否开启分栏预览
705-
appViewModel.isSplitPreviewEnabled = settings.defaultSplitPreview
706-
}
707-
}
708-
.onChange(of: documentViewModel.currentFileURL) { _, _ in
709-
previewContent = documentViewModel.content
710-
}
711-
.task(id: documentViewModel.content) {
712-
// 击键防抖:停顿 200ms 无新输入后再刷新右侧预览。
713-
// 分栏已关闭时不写 previewContent:避免对已拆毁的预览 WebView 触发无谓重算,
714-
// 也避免与视图树拆毁竞态。
715-
try? await Task.sleep(for: .milliseconds(200))
716-
guard !Task.isCancelled else { return }
717-
guard appViewModel.isSplitPreviewEnabled else { return }
718-
previewContent = documentViewModel.content
719-
}
720674
.overlay(alignment: .topTrailing) {
721675
if appViewModel.isFindBarVisible, documentViewModel.hasDocument {
722676
FindReplaceBar(
@@ -734,6 +688,30 @@ struct DetailView: View {
734688
}
735689
}
736690
.animation(.easeInOut(duration: 0.2), value: appViewModel.isFindBarVisible)
691+
.overlay(alignment: .topTrailing) {
692+
// 编辑模式内容区右上角原生复制按钮。
693+
// 仅 raw 模式 + 有文档 + 查找栏关闭 + 内容一键复制总开关开启时显示,
694+
// 避免与查找栏浮层重叠,并遵从总开关。
695+
if documentViewModel.hasDocument
696+
&& documentViewModel.displayMode == .raw
697+
&& !appViewModel.isFindBarVisible
698+
&& settings.enableDocumentCopy {
699+
Button {
700+
copyRawContent()
701+
} label: {
702+
Image(systemName: contentCopyState.isShowingSuccess ? DocumentCopySymbol.copied.rawValue : DocumentCopySymbol.copy.rawValue)
703+
.font(.system(size: 11))
704+
.frame(width: 14, height: 14)
705+
.foregroundStyle(contentCopyState.isShowingSuccess ? themeColors.success : themeColors.fgMuted)
706+
.frame(width: 24, height: 24)
707+
}
708+
.buttonStyle(.plain)
709+
.help(L10n.tr(contentCopyState.isShowingSuccess ? .contentCopied : .contentCopy, language: language))
710+
.padding(.trailing, 11)
711+
.padding(.top, 3)
712+
.transition(.opacity)
713+
}
714+
}
737715
.animation(.easeInOut(duration: 0.2), value: contentCopyState.isShowingSuccess)
738716
.onChange(of: findReplaceViewModel.searchText) { _, _ in performSearch() }
739717
.onChange(of: findReplaceViewModel.isCaseSensitive) { _, _ in performSearch() }
@@ -746,47 +724,6 @@ struct DetailView: View {
746724
.onChange(of: settings.enableDocumentCopy) { _, _ in invalidateCopyFeedback() }
747725
}
748726

749-
/// 渲染视图(渲染模式全宽显示 / 编辑模式右栏实时预览共用)
750-
@ViewBuilder
751-
private func webMarkdownView(content: String, exportedPage: Binding<WebPage?>) -> some View {
752-
WebViewMarkdownView(
753-
content: content,
754-
fileURL: documentViewModel.currentFileURL,
755-
contentPadding: settings.contentPaddingPoints,
756-
maxContentWidthFollowsWindow: settings.maxContentWidthFollowsWindow,
757-
scrollToLine: documentViewModel.scrollToLineRequest,
758-
themeCSS: themeColors.cssCustomProperties + themeColors.codeHighlightCSS,
759-
isDark: settings.resolvedThemeType == .dark,
760-
// 仅渲染模式显示 WebView 内复制按钮;分栏预览时右栏不显示,
761-
// 复制按钮保留在左侧 Markdown 源码栏(原生 overlay)。
762-
documentCopyEnabled: settings.enableDocumentCopy && documentViewModel.displayMode == .rendered,
763-
searchQuery: findReplaceViewModel.searchText,
764-
searchCaseSensitive: findReplaceViewModel.isCaseSensitive,
765-
searchWholeWord: findReplaceViewModel.isWholeWord,
766-
searchCurrentIndex: findReplaceViewModel.currentMatchIndex,
767-
isFindBarVisible: appViewModel.isFindBarVisible,
768-
contentVersion: documentViewModel.contentVersion,
769-
onVisibleHeadingChanged: { heading in
770-
activeOutlineLineNumber = heading?.lineNumber
771-
},
772-
onVisibleLineChanged: { lineNumber in
773-
documentViewModel.renderedVisibleLineNumber = lineNumber
774-
},
775-
commandTarget: commandTarget,
776-
onOpenLinkedMarkdownFile: { [weak session] url in
777-
session?.handleLinkedMarkdownFile(url.standardizedFileURL)
778-
},
779-
exportedPage: exportedPage
780-
)
781-
.onChange(of: documentViewModel.scrollToLineRequest) { _, newValue in
782-
if newValue != nil {
783-
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
784-
documentViewModel.clearScrollRequest()
785-
}
786-
}
787-
}
788-
}
789-
790727
/// 把 find/reload/exportPDF handler 注册到注入的本窗口命令目标上。
791728
///
792729
/// 回归修复根因 1:DetailView 不再发布独立 `focusedSceneValue(\.windowCommandTarget, …)`

Sources/MarkdownReader/Views/SettingsView.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,12 @@ struct GeneralSettingsView: View {
5252
SettingsSection(
5353
title: L10n.tr(.settingsGeneralDisplayMode, language: language)
5454
) {
55-
HStack(spacing: 12) {
56-
Picker("", selection: $settings.defaultDisplayMode) {
57-
Text(L10n.tr(.displayModeRendered, language: language)).tag(DisplayMode.rendered)
58-
Text(L10n.tr(.displayModeRaw, language: language)).tag(DisplayMode.raw)
59-
}
60-
.pickerStyle(.segmented)
61-
.frame(width: 200)
62-
63-
// 常驻显示:进入编辑模式时是否默认开启分栏预览
64-
Toggle(L10n.tr(.settingsGeneralDefaultSplitPreview, language: language), isOn: $settings.defaultSplitPreview)
55+
Picker("", selection: $settings.defaultDisplayMode) {
56+
Text(L10n.tr(.displayModeRendered, language: language)).tag(DisplayMode.rendered)
57+
Text(L10n.tr(.displayModeRaw, language: language)).tag(DisplayMode.raw)
6558
}
59+
.pickerStyle(.segmented)
60+
.frame(width: 200)
6661
}
6762

6863
SettingsDivider()

0 commit comments

Comments
 (0)