Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions scripts/largeFileLoadRevision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ g.$derived.by = (fn: () => unknown) => fn();
g.$effect = runeEffect;
g.window = g.window ?? {};

const PREVIEW_BYTES = 50000;
const FULL = `# big\n\n${'x'.repeat(PREVIEW_BYTES)}\n\ntail that must never be lost\n`;
const PARTIAL = FULL.slice(0, PREVIEW_BYTES);
// The preview always returns isFull=false to exercise the two-stage path
// regardless of the actual threshold — no need for a multi-MB test string.
const FULL = '# big\n\n' + 'x'.repeat(200) + '\n\ntail that must never be lost\n';
const PARTIAL = FULL.slice(0, 40) + '…';

/** Per-call delays, so the two concurrent loads can be ordered deliberately. */
let previewDelays: number[] = [];
Expand Down Expand Up @@ -88,6 +89,7 @@ function makeSession() {
askClose: async () => 'discard' as const,
onCloseSaveNewerEdits: () => {},
onCloseAutoSaveFailed: () => {},
onPartialCopySaved: () => {},
});
}

Expand All @@ -101,6 +103,7 @@ function reset() {
if (cmd === 'canonicalize_path') return '/docs/big.md';
if (cmd === 'open_markdown_preview') {
const delay = previewDelays[previewCall++] ?? 0;
// Always return isFull=false to exercise the two-stage load path
return wait(delay).then(() => ['<p>preview</p>', PARTIAL, false, false, 'UTF-8']);
}
if (cmd === 'read_file_content_checked') return [FULL, false, 'UTF-8'];
Expand All @@ -123,7 +126,7 @@ async function loadTwice() {
return { session, tab: tabManager.activeTab! };
}

test('an overtaken load cannot leave the tab holding its 50KB slice', async () => {
test('an overtaken load cannot leave the tab holding its preview slice', async () => {
reset();
// The first load's preview read is slow, so it lands after the second load
// has already completed the tab. This is the ordering that stranded it.
Expand Down
16 changes: 8 additions & 8 deletions src/lib/sessions/documentSession.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function markPreviewMatchesBuffer(tab: Tab, rawContent: string) {

/**
* Which heading sections the tab being loaded has folded, read at render time
* rather than captured up front. A large file is rendered twice — the 50KB
* rather than captured up front. A large file is rendered twice — the 5MB
* preview, then the whole document once the background read lands — and the
* user can fold something in between; the second render has to honour that.
* An unknown tab folds nothing, which is what a load of a tab that has since
Expand Down Expand Up @@ -197,7 +197,7 @@ export function createDocumentSession(options: DocumentSessionOptions) {
}

/**
* Replace a partial buffer (the >50KB preview read) with the whole file.
* Replace a partial buffer (the >5MB preview read) with the whole file.
* Must be awaited by every path that can lead to a write — entering the
* editor or split view, editing front matter, toggling a task checkbox,
* moving the tab to another window — because writing the partial buffer
Expand Down Expand Up @@ -225,7 +225,7 @@ export function createDocumentSession(options: DocumentSessionOptions) {
// CLEARS the flag for a file converted to UTF-8 since the load.
const [full, lossy, encoding] = (await invoke('read_file_content_checked', { path: tab.path })) as [string, boolean, string];
tabManager.setTabDecodedLossy(tabId, lossy);
// The preview's answer came from the first 50KB; this one is the
// The preview's answer came from the first 5MB; this one is the
// whole file's, and it is the one every save from here on uses.
tabManager.setTabEncoding(tabId, encoding);
tabManager.setTabRawContent(tabId, full);
Expand Down Expand Up @@ -442,7 +442,7 @@ export function createDocumentSession(options: DocumentSessionOptions) {
// both as well), so two loads can run on one tab. The full-load stage
// already refuses to apply a stale result; the first stage did not, and
// an older preview landing last overwrote the winner's complete buffer
// with its 50KB slice, re-raising `isTruncated` — after which every save
// with its 5MB slice, re-raising `isTruncated` — after which every save
// is refused and nothing retries. Same revision test, applied to every
// write a load makes.
const isCurrentLoad = () => loadRevisionByTab.get(activeId) === fullLoadRevision;
Expand All @@ -463,14 +463,14 @@ export function createDocumentSession(options: DocumentSessionOptions) {
// And both are applied BEFORE `initialIsSplit` is read, so a
// document opening into either editable mode takes the
// full-read branch below: those panes can write, and an editor
// bound to the 50KB preview slice is one keystroke away from
// bound to the 5MB preview slice is one keystroke away from
// auto-saving it back over the whole file.
tab.isEditing = settings.openFileMode === 'editor';
if (settings.openFileMode === 'split') tabManager.setSplitEnabled(tab.id, true);
}
const initialIsEditing = tab?.isEditing ?? false;
const initialIsSplit = tab?.isSplit ?? false;
// `open_markdown_preview` returns only the first 50KB of a large
// `open_markdown_preview` returns only the first 5MB of a large
// file, which is fine behind a read-only preview because the
// background read below completes it. An editor bound to that
// partial buffer is one keystroke away from auto-saving it back
Expand All @@ -485,7 +485,7 @@ export function createDocumentSession(options: DocumentSessionOptions) {
[content, lossy, encoding] = (await invoke('read_file_content_checked', { path: filePath })) as [string, boolean, string];
isFull = true;
} else {
[, content, isFull, lossy, encoding] = (await invoke('open_markdown_preview', { path: filePath, maxBytes: 50000 })) as [string, string, boolean, boolean, string];
[, content, isFull, lossy, encoding] = (await invoke('open_markdown_preview', { path: filePath, maxBytes: 5_000_000 })) as [string, string, boolean, boolean, string];
}
// Ahead of the encoding verdict, not just the buffer: a prefix's
// detected encoding can differ from the whole file's, and
Expand Down Expand Up @@ -527,7 +527,7 @@ export function createDocumentSession(options: DocumentSessionOptions) {
tabManager.setTabRawContent(activeId, fullContent);
// This buffer REPLACES the preview's, so it
// carries its own verdict — the preview only
// saw the first 50KB, and both the fidelity
// saw the first 5MB, and both the fidelity
// and the detected encoding of a prefix can
// differ from the whole file's.
tabManager.setTabDecodedLossy(activeId, fullLossy);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/tabs.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export interface Tab {
collapsedHeaders: Set<string>;
/**
* True while `rawContent` holds only the leading slice of a large file
* (the >50KB preview read) instead of the whole document. Such a buffer
* (the >5MB preview read) instead of the whole document. Such a buffer
* looks clean and authoritative but writing it back truncates the file,
* so every path that can reach disk must complete it first — see
* `ensureFullContent` in documentSession. Optional because tabs built
Expand Down