From 760c22fd4b25b63175dd31ddd8b1dc6396b293cf Mon Sep 17 00:00:00 2001 From: rinh Date: Thu, 22 Oct 2026 12:02:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(reload):=20Reload=20the=20document=20from?= =?UTF-8?q?=20disk=20on=20demand=20(=E2=8C=98R)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-reload relies on file-system events, which some network volumes and editors' save strategies never deliver; the only way back to the current text was closing and reopening the document. View ▸ Reload (⌘R) and a ↻ pill in the top-right cluster re-read the file now and restart the watcher, so a watcher that lost track of the file recovers too. A toast says whether anything changed ("Reloaded" / "Reloaded · no changes"); a missing file shows the existing "File no longer exists" banner. Disabled while Source Edit shows: the session already follows the disk and owns the conflict banner, so a second path to the same file would race it. Routed through @FocusedValue like every other per-document command, so ⌘R acts on the frontmost tab only. Co-Authored-By: Claude Opus 5.5 --- QuickMD/CHANGELOG.md | 5 +++++ QuickMD/QuickMD/MarkdownExport.swift | 9 +++++++++ QuickMD/QuickMD/MarkdownView.swift | 20 ++++++++++++++++++++ QuickMD/QuickMD/QuickMDApp.swift | 18 ++++++++++++++++++ QuickMD/QuickMD/Views/ChromeButtons.swift | 16 ++++++++++++++++ README.md | 1 + 6 files changed, 69 insertions(+) diff --git a/QuickMD/CHANGELOG.md b/QuickMD/CHANGELOG.md index 20a8543..6cc3030 100644 --- a/QuickMD/CHANGELOG.md +++ b/QuickMD/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to QuickMD will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- **Reload** (⌘R, View ▸ Reload, or the ↻ button in the top-right cluster): re-reads the document from disk now and restarts the file watcher, for the times auto-reload does not notice a change (some network volumes and editors' save strategies). A toast says whether anything changed; a file that is gone shows the "File no longer exists" banner. Off while Source Edit shows, where the editor already follows the disk. + ## [1.12.0] - 2026-10-05 Source Edit: QuickMD stays a viewer first, but you can now fix the Markdown you are reading without leaving the window. Asked for by [@KillSwitchSIG](https://github.com/KillSwitchSIG) (#31) and [@vinaymve](https://github.com/vinaymve) (#34). diff --git a/QuickMD/QuickMD/MarkdownExport.swift b/QuickMD/QuickMD/MarkdownExport.swift index c874d0c..f5bf0b6 100644 --- a/QuickMD/QuickMD/MarkdownExport.swift +++ b/QuickMD/QuickMD/MarkdownExport.swift @@ -28,6 +28,11 @@ struct FocusedOpenInExternalEditorKey: FocusedValueKey { typealias Value = () -> Void } +/// View ▸ Reload (⌘R) and the reload pill. +struct FocusedReloadDocumentKey: FocusedValueKey { + typealias Value = () -> Void +} + struct FocusedZoomActionKey: FocusedValueKey { typealias Value = (MarkdownZoom) -> Void } @@ -147,6 +152,10 @@ extension FocusedValues { get { self[FocusedOpenInExternalEditorKey.self] } set { self[FocusedOpenInExternalEditorKey.self] = newValue } } + var reloadDocumentAction: (() -> Void)? { + get { self[FocusedReloadDocumentKey.self] } + set { self[FocusedReloadDocumentKey.self] = newValue } + } var zoomAction: ((MarkdownZoom) -> Void)? { get { self[FocusedZoomActionKey.self] } set { self[FocusedZoomActionKey.self] = newValue } diff --git a/QuickMD/QuickMD/MarkdownView.swift b/QuickMD/QuickMD/MarkdownView.swift index e9bfc52..e5b2ff8 100644 --- a/QuickMD/QuickMD/MarkdownView.swift +++ b/QuickMD/QuickMD/MarkdownView.swift @@ -434,6 +434,9 @@ struct MarkdownView: View { .transition(.opacity) } if documentURL != nil { + ReloadDocumentButton(theme: theme) { + reloadManually() + } EditSourceButton(theme: theme) { toggleSourceEdit() } @@ -526,6 +529,7 @@ struct MarkdownView: View { }) .focusedSceneValue(\.copyDocumentAction, { copyToClipboard(currentText) }) .focusedSceneValue(\.openInExternalEditorAction, { openInExternalEditor() }) + .focusedSceneValue(\.reloadDocumentAction, { reloadManually() }) .focusedSceneValue(\.toggleDocumentListAction, { guard !isReadingMode else { return } withAnimation(.easeInOut(duration: 0.2)) { isDocumentListVisible.toggle() } @@ -844,6 +848,22 @@ struct MarkdownView: View { } } + /// ⌘R / the reload pill: re-read the file now, and restart the watcher in + /// case it lost track of the file (some volumes and save strategies never + /// deliver an event). Not while Source Edit shows — the session owns the + /// disk then. + private func reloadManually() { + guard !editSession.isActive, let url = documentURL else { return } + guard FileManager.default.fileExists(atPath: url.path) else { + withAnimation(.easeInOut(duration: 0.2)) { fileMissing = true } + return + } + let before = currentText + reloadFromDisk() + startWatching(url) + showToast(currentText == before ? "Reloaded \u{00B7} no changes" : "Reloaded") + } + /// ⌘E — hand the document off to the user's configured editor, at the line /// the reader is at when that editor documents a line link (v1.11 E-D1). private func openInExternalEditor() { diff --git a/QuickMD/QuickMD/QuickMDApp.swift b/QuickMD/QuickMD/QuickMDApp.swift index 076917d..5a47789 100644 --- a/QuickMD/QuickMD/QuickMDApp.swift +++ b/QuickMD/QuickMD/QuickMDApp.swift @@ -48,6 +48,8 @@ struct QuickMDApp: App { CopyMarkdownCommand() } CommandGroup(after: .toolbar) { + ReloadDocumentCommand() + Divider() ZoomCommands() ToggleToCCommand() ToggleDocumentListCommand() @@ -209,6 +211,22 @@ struct CopyMarkdownCommand: View { } } +/// View ▸ Reload (⌘R) — re-reads the frontmost document from disk, for the +/// times auto-reload did not notice a change (network volumes, some editors' +/// save strategies). Off while Source Edit shows: the session owns the disk. +struct ReloadDocumentCommand: View { + @FocusedValue(\.reloadDocumentAction) var reloadDocumentAction + @FocusedValue(\.sourceEditState) var sourceEditState + + var body: some View { + Button("Reload") { + reloadDocumentAction?() + } + .keyboardShortcut("r", modifiers: .command) + .disabled(reloadDocumentAction == nil || sourceEditState?.isEditing == true) + } +} + struct OpenInExternalEditorCommand: View { @FocusedValue(\.openInExternalEditorAction) var openInExternalEditorAction @FocusedValue(\.sourceEditState) var sourceEditState diff --git a/QuickMD/QuickMD/Views/ChromeButtons.swift b/QuickMD/QuickMD/Views/ChromeButtons.swift index 00f4351..6938728 100644 --- a/QuickMD/QuickMD/Views/ChromeButtons.swift +++ b/QuickMD/QuickMD/Views/ChromeButtons.swift @@ -319,6 +319,22 @@ struct CopySourceButton: View { } } +// MARK: - Reload Button + +/// Top-right button: re-read the document from disk (⌘R). +struct ReloadDocumentButton: View { + let theme: MarkdownTheme + let action: () -> Void + + var body: some View { + ChromePill(theme: theme, title: "Reload", help: "Reload from disk (⌘R)", action: action) { + Image(systemName: "arrow.clockwise") + .font(.system(size: 11)) + } + .accessibilityIdentifier("reload-document") + } +} + // MARK: - Edit Source Button /// Top-right pencil button — fixes the source in place (Source Edit, ⌥⌘E). diff --git a/README.md b/README.md index 01b6229..1116d95 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Perfect for developers, writers, students, and anyone who works with Markdown da ### Works with Your Editor - **Auto-reload** — the document refreshes the moment another app saves it. Turn on auto-save in VS Code, Cursor or Zed, or let an AI agent write the file, and QuickMD is a live preview +- **Reload (`⌘R`)** — re-read the file on demand, for the rare change auto-reload misses (network volumes, unusual save strategies) - **Open in External Editor (`⌘E`)** — for longer writing sessions: hands the file to your editor (auto-detected, configurable in Settings). VS Code, BBEdit, TextMate and Nova open at the line you are reading ### Complete Markdown Support