fix(editor): show the preview without making the user save first - #421
Merged
Conversation
Leaving the editor re-read the file from disk, so a dirty tab had to resolve a save decision - silently write, or answer a modal - before the rendered view could appear. dayeggpi reported this in #168: "no way to see rendered view until file is saved." The prompt was never about losing data. Nothing is lost by switching view mode; the buffer stays in memory either way. It existed because the exit path called `loadMarkdown(tab.path)`, and reading the disk on a dirty tab would show the wrong text. The untitled branch two lines below already rendered the buffer instead - and #407 extracted `renderTabPreviewFromRaw` for the print path, which is exactly the same operation with a real path. Both exits now use it. That also removes three problems the disk read carried: the exit took the 50KB preview branch, so leaving the editor on a large file re-truncated a complete buffer and refused saves until the background read finished; `loadMarkdown` writes into the *active* tab, so `toggleSplitView(tabId)` on a background tab would have yanked the active one; and `if (!success) return` kept the tab in edit mode when the write failed - which for a read-only file or a lossily decoded buffer meant reading mode was permanently unreachable. One segment of that block is kept, narrowed to `autoSave && !confirmBeforeSave`: the auto-save effect treats a tab as writable only while `isEditing || isSplit`, and clears its pending timer otherwise, so leaving edit mode drops the scheduled write. That flush is the last chance before the window closes, not a condition of the switch. Closing a tab and closing a window still ask. Those buffers are about to cease to exist; this one is not. VS Code, Obsidian and Typora all render the buffer, and none of them asks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 3, 2026
Closed
PathGao
added a commit
that referenced
this pull request
Aug 3, 2026
…#441) `Tab` carries three same-shaped strings. `rawContent` is the Markdown, and it is what the editor edits, what reaches disk, and what the exports read. `content` is the rendered preview HTML, injected via `{@html}`. Two places read the wrong one. 1. TitleBar.svelte gated Export as HTML / Export as PDF on `tabManager.activeTab.content`, while `exportAsHtml` gates on `ctx.rawContent`. `content` is only refreshed while the preview is on screen — `tab.isSplit || (isEditing && settings.showToc)` — so for an untitled buffer being edited with the TOC closed it is still `''` while `rawContent` holds the user's text. `showToc` and `newFileDefaultMode` default such that Ctrl+T then typing is exactly that state, and the menu hid two commands that would have produced a file. Reachable since #421 let the preview work without saving first. 2. `addTab(path, content = '')` assigned its argument to `content`, `rawContent` and `originalContent` alike — coherent when the three were one field, but it let Markdown into the field that is injected as HTML. Both callers in the app pass `''`, so nothing shipped broken; the value is sanitized at the sink either way. The parameter is now `rawContent` and `content` starts empty, as at every other Tab construction site. scripts/renderedHtmlField.test.ts evaluates the real gate, the real refresh condition and `exportAsHtml`'s real precondition against the real TabManager. Four of its nine tests are red on master. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the second item of #168 (thanks @dayeggpi):
The prompt was never about losing data
Leaving the editor called
loadMarkdown(tab.path, …), which re-reads the file from disk. On a dirty tab that would show the wrong text, so the code first resolved a save decision — write silently, or put up a modal.Nothing is lost by switching view mode: the buffer stays in memory either way. The save existed to keep the screen from lying, not to protect the document. And the untitled branch two lines below already rendered the buffer instead:
#407 already extracted
renderTabPreviewFromRaw(tab)for the print path — the same operation with a real path. Both exits now use it. The parts are all there; this is one wire.git log -Splaces the modal in85c6e5e fix: flush dirty tabs on window close— a window-close fix. The correct pattern for "don't lose edits when the buffer disappears" was applied in the same commit to a case where the buffer does not disappear.Three more problems the disk read carried
isEditingalreadyfalse, so it took the 50 KB preview branch: leaving the editor on a large file re-truncated a complete buffer, flagged itisTruncated(saves refused during that window), then re-read in the background.loadMarkdownalways writes into the active tab, sotoggleSplitView(tabId)on a background tab would have pulled the active tab's content over. No caller reaches that today; the trap is gone.if (!success) returnkept the tab in edit mode when the write failed.saveContentreturnsfalsepermanently for a read-only path or a lossily decoded buffer, so reading mode was unreachable for that tab, forever. A failed write is not a reason to hide the user's own text.What was kept, and why
autoSave && !confirmBeforeSave. It is load-bearing for an independent reason: the auto-save effect treats a tab as writable only whileisEditing || isSplitand clears its pending timer otherwise, so leaving edit mode drops the scheduled write. This is the last flush before that window closes — not a condition of the switchsilentSaveforcing a write when auto-save is offaskCustommodaldiscardoption reverted the buffer tooriginalContent— a destructive choice offered for a switch that destroys nothing.confirmBeforeSavepromises confirmation before each write; with no write there is nothing to confirmsavedNewerEditstoastreturnis gone: the three reasons for it are stale (Cmd+S in reading mode was fixed earlier in this same #168 round; a dirty tab is already protected from disk reload byresolveExternalChange; auto-save being off is now the intended steady state). What survives is the real signal — after a flush, a still-dirty tab means the file is one revision behindWhat
loadMarkdownalso didTraced line by line; it is not modified. Lost and judged safe:
setShowHome(false)(unreachable state), the load-revision bump (canApplyFullLoadalready requires a clean tab),setTabDecodedLossy(entering edit mode sets it, and not re-reading means the verdict has not changed). Gated and never fired:resetScrollHistory,navigate,updateLoading. Must not have run:setTabRawContent, which replacesoriginalContentand clearsisDirty— it would have erased the edits, and the dirty short-circuit would have left a stale preview anyway.One deliberate behaviour delta: a view toggle no longer bumps the file in Recent. The entry exists from the actual open; a mode toggle is not an open.
Closing still asks
canCloseTabandappExitare byte-for-byte untouched, and two tests assert they still prompt — executed against the realcanCloseTab. Those buffers are about to cease to exist. This one is not.Mainstream agrees, and on the shape rather than the detail — VS Code follows the in-memory document and previews an untitled buffer; Obsidian makes reading view a plain toggle on the same key Markpad binds, documenting no save step; Typora has no preview/file distinction at all. No new "this is unsaved" indicator: the tab's dirty dot is the standing signal, and adding a reading-mode banner would be a Markpad-only invention for a state every other editor treats as unremarkable.
Tests
scripts/viewModeWithoutSaving.test.tsextracts the realtoggleEdit/toggleSplitView/renderTabPreviewFromRawfrom the component with a string- and comment-aware brace slicer, transpiles with the project's owntypescript, and runs them against the realTabManager. The fake disk reproducesloadMarkdown's dirty short-circuit and serves text that differs from the buffer, so a disk route shows up in the rendered output, not just in a call log.Sample reds:
no unsaved-changes modal on a view toggle→ actual['modal.youHaveUnsavedChangesBeforeReturning']·the file is not re-read to leave the editor→ actual['/notes/note.md']·but the view still switches→true !== false(the unwritable-file trap).The 5 green on both sides are the boundary: closing a tab still asks and Cancel still keeps it open, closing the window still reviews unsaved tabs, auto-save still flushes, untitled still never hits the Save dialog.
Orphaned keys, not deleted
modal.youHaveUnsavedChangesBeforeReturningandmodal.youHaveUnsavedChangesBeforeClosingSplitViewnow have zero usages but are still defined in 26 locales.i18nCoveragestays green (it fails on keys English lacks, not on unused ones). Left for whoever prunes the dictionary.Not covered
toast.autoSaveFailedon top of the guard's own explanation. The background auto-save suppresses this viaisLossySaveRefused; the toggle path never did, before or after. Pre-existing, but slightly more visible now that the transition proceeds instead of stopping.🤖 Generated with Claude Code