Skip to content

docs(tabs): say what content, rawContent and originalContent each hold - #437

Merged
PathGao merged 1 commit into
masterfrom
docs/tab-buffer-fields
Aug 3, 2026
Merged

docs(tabs): say what content, rawContent and originalContent each hold#437
PathGao merged 1 commit into
masterfrom
docs/tab-buffer-fields

Conversation

@PathGao

@PathGao PathGao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

The Tab interface documents its four newest fields at 8–23 lines each and its 17 original fields at zero between them. pathKey gets 23 lines; the question a first-time reader actually arrives with — what is the difference between content, rawContent and originalContent? — gets none, and those three drive the entire dirty/save/render system.

This is comments only. npm test 524/524, npm run check 630 files / 0 errors, npm run build clean.

The distinction, with the evidence

what it holds written by read by
rawContent the live Markdown updateTabRawContent, setTabRawContent, the editor binding editor, preview, exports, save_file_content
originalContent that buffer as it last came off / went onto disk setTabRawContent, saveContent, saveContentAs isDirty only
content rendered preview HTML updateTabContent {@html sanitizedHtml}

rawContent is the document. The editor two-way binds to it — bind:value={tabManager.activeTab.rawContent} (MarkdownViewer.svelte:3288) — the preview renders from it (renderTabPreviewFromRaw, MarkdownViewer.svelte:831-834), the HTML export renders from it (export.ts:284, gated on ctx.rawContent at export.ts:376), and it is the exact string that reaches disk: const snapshot = tab.rawContentinvoke('save_file_content', { content: snapshot }) (documentSession.svelte.ts:423-427, and again at :464-468 for Save As).

originalContent exists only to answer "changed?". isDirty is literally the comparison — tab.isDirty = tab.rawContent !== tab.originalContent (tabs.svelte.ts:496, now line 534) — and discarding edits is the assignment tab.rawContent = tab.originalContent (documentSession.svelte.ts:523). It has no other reader. git show 4c4e6f1 is the commit that introduced it, and it is the whole story: it replaced tab.isDirty = true with that comparison, turning a latch into a computed answer.

content is not Markdown. It is the output of renderMarkdownPreview, cached per tab and re-sanitized at the sink (htmlContentsanitizedHtml, MarkdownViewer.svelte:233/244, injected at :3445). git show 0694991 shows why the naming is backwards: content predates the editor, and when Monaco arrived rawContent was added beside it for the source rather than renaming anything. tabTransfer.ts:16 already says this correctly for the transfer payload.

content is allowed to lag rawContent. The re-render effect only runs when tab.isSplit || (isEditing && settings.showToc) (MarkdownViewer.svelte:2358), and syncPreviewForPrint (:1935-1975) exists specifically to repair that before an export prints the live DOM.

"Clean" does not mean "matches disk". setTabRawContent assigns the read to both buffers and clears isDirty (tabs.svelte.ts:509-517), and it is called with the 50KB preview slice on a large-file open (documentSession.svelte.ts:329). markTabContentUnavailable does the same with '' when the read fails (tabs.svelte.ts:535-542). Both cases leave a tab that is not dirty and is not the file — which is exactly what isTruncated is for.

Assigning rawContent directly does not maintain isDirty. The editor writes through bind:value and calls tabManager.updateTabRawContent (components/Editor.svelte:323-331); the paths that assign the field directly maintain the flag themselves (documentSession.svelte.ts:432-433, :475-476, :523-524; MarkdownViewer.svelte:482-483).

Also documented: history / historyIndex

Four lines, chosen because it is the one remaining undocumented field whose name actively misleads in the same way content does: sitting between three text buffers and editorViewState, history: string[] reads as undo history. It is the back/forward file path stack — createFileHistory returns [path], goBackInHistory returns history[historyIndex] and the caller navigates to it (utils/tabHistory.ts), and navigate pushes targetPath (tabs.svelte.ts:713-737). Text undo is Monaco's (components/Editor.svelte:1517-1524). createFileHistory(path, _content = '') still takes an ignored content parameter, which suggests the confusion is not hypothetical.

Nothing else was documented. id, title, isDirty, isSplit, splitRatio and the rest are self-evident, and comment volume is the cost this change is reacting to.

Traced

Fresh open (full and >50KB), keystroke, save, Save As, external-change reload and conflict, failed read, tab switch, cross-window transfer, session restore, discard-on-close. Each ends in one of setTabRawContent / updateTabRawContent / the two explicit originalContent = snapshot sites, and no path writes content or originalContent to disk.

Not traced

  • Live mode (liveMode) and the front-matter editor's rewrites beyond confirming they route through updateTabRawContent (MarkdownViewer.svelte:885, :967).
  • scrollTop / scrollPercentage / anchorLine. Three fields for one concept, undocumented, and plausibly the second question this struct raises — left alone deliberately rather than overlooked.
  • The Rust side of open_markdown_preview / read_file_content_checked; taken at their TypeScript call signatures.

Two things that do not fit cleanly (reported, not fixed)

1. TitleBar.svelte:480 gates Export HTML / Export PDF on the rendered-HTML cache.

{#if currentFile !== '' || (tabManager.activeTab && tabManager.activeTab.content)}

exportAsHtml itself gates on ctx.rawContent (export.ts:376). For an unsaved untitled buffer being edited with the TOC closed, currentFile is '' and content is '' — nothing has re-rendered since the tab was created — while rawContent holds the user's text. The menu hides an export that the export function would accept. This is a content/rawContent mix-up of exactly the kind the new comment describes, and it became reachable when #421 let the preview work without a save. Fixing it is a behaviour change and is out of scope here.

2. addTab(path, content = '') still seeds content from its content argument (tabs.svelte.ts:342, alongside rawContent: content and originalContent: content). That was coherent when the three fields were one; now it means the signature permits putting raw Markdown into the field that is injected as HTML. Both live callers pass '' or nothing (documentSession.svelte.ts:254, MarkdownViewer.svelte:1428), so it is latent, not live. Not renamed or re-signatured here.

No existing comment was found to contradict these findings. tabTransfer.ts:16 (content = rendered HTML) and the MarkdownViewer.svelte:1789 claim that "every keystroke flows through updateTabRawContent" both check out.

🤖 Generated with Claude Code

The Tab interface documents its four newest fields at 8-23 lines each and
its 17 original fields at zero. The first question the struct actually
raises is the difference between `content`, `rawContent` and
`originalContent` - three same-shaped strings that drive the whole
dirty/save/render system - and nothing answered it.

One block above the three, stating which one to use and what breaks if
you pick another, plus four lines on `history`/`historyIndex` because
that field's name says undo and it holds file paths.

Comments only. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit da098b9 into master Aug 3, 2026
4 checks passed
@PathGao
PathGao deleted the docs/tab-buffer-fields branch August 3, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant