fix(tabs): one tab per file path - #413
Merged
Merged
Conversation
`navigate`, `updateTabPath`, `renameTab`, `goBack`, `goForward` and `insertTransferredTab` all assigned `tab.path` without checking whether another tab already held it, and `openMarkdownTargetInNewTab` called `addTab` unconditionally - so "open link in new tab" on an already-open file, or "Save As" onto one, produced two tabs on one path. Each then carried its own dirty flag and its own auto-save timer, and they wrote over each other. Neither VS Code nor Sublime forbids two tabs; both forbid two buffers. VS Code keys one `ITextModel` per URI and lets editors be views onto it; Sublime's `clone_file` explicitly makes a second view of the same buffer. Markpad has no buffer/view split - a tab *is* the buffer - so the faithful translation of that invariant is one tab per path, which is also what `loadMarkdown` already did on the ordinary open path. Conflicts are resolved by what the loser stands to lose. A clean loser is closed: its buffer is a copy of the file the winner now holds, so nothing is lost, and it lands on the reopen-closed-tab stack like any other close. A dirty loser is kept and releases the path instead, becoming untitled with its buffer and title intact - nothing is discarded, nothing can auto-save over the file behind the user's back, and saving it asks where to put it, which is the one decision only the user can make. Refusing the claim and activating the existing tab - the naive reading of what VS Code does - would be worse than the bug: `loadMarkdown` captures `activeId` before calling `navigate` and writes the newly read content into that captured tab, so declining the path change hands one document's text to a tab pointing at another file. 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.
The defect
navigate,updateTabPath,renameTab,goBack,goForwardandinsertTransferredTaball assignedtab.pathwithout checking whether another tab already held it, andopenMarkdownTargetInNewTabcalledaddTabunconditionally. So:…gave you two tabs on one path. Each then carried its own
isDirtyand its own auto-save timer, and they wrote over each other.loadMarkdownalready de-duplicated, but only on the ordinary open path.What the invariant should be
Neither VS Code nor Sublime forbids two tabs. Both forbid two buffers.
ITextModelper URI; editors are views onto it, so edits appear in all of them and there is exactly one save path. Two divergent buffers for one URI is not a representable state.clone_file, which explicitly creates a second view of the same buffer — never a second buffer.Markpad has no buffer/view split — a tab is the buffer — so the faithful translation of that invariant is one tab per path. Only real file paths are exclusive; several untitled tabs remain normal.
Conflict resolution, by what the loser stands to lose
Why not "refuse the claim and activate the existing tab"
That is the naive reading of what VS Code does, and here it would be worse than the bug.
loadMarkdowncapturesactiveIdbefore callingnavigate, then writes the newly read content into that captured tab. Declining the path change leaves the tab pointing at file A while receiving file B's text — and the next save writes B over A.Why this is safe now, and was not before
De-duplicating means "open link in new tab" resolves to an already-open tab and hands it to a loader that previously would have written disk content into a brand-new one. If that tab has unsaved edits, the de-duplication is what makes them reachable.
#412 closes that. The receipt is a single test, deliberately narrowed to one assertion so its verdict is unambiguous:
following a link into a new tab never costs the target tab its unsaved editsmasteraddTabmakes a second tab, so nothing is overwrittenI ran that third state explicitly rather than reasoning about it.
Tests
scripts/tabPathIdentity.test.ts(10) plus one assertion added toreopenDirtyDocument.test.ts.By file:
tabPathIdentity7 red / 3 green — the three greens are the don't-over-fire boundaries (untitled tabs do not collide, the HOME sentinel is a singleton on its own terms, and "re-opening doesn't overwrite edits", which passes on #412 precisely because #412 landed).reopenDirtyDocument1 red / 11 green, the red being the new tab-identity assertion.Not covered
/notes/A.mdand/notes/a.mdare two files on macOS and Windows.refuseIfLossilyDecodedand fix(documents): never reload a file over its own unsaved edits #412 document the same gap; all three need the backend to canonicalise.🤖 Generated with Claude Code