feat(viewer): viewer window UI with live reload, save and close guard
EditorPane loads the resolved file, polls it every 2 s while visible, reloads a clean buffer silently and shows the "Changed on disk" banner for a dirty one, saves against the loaded hash, and intercepts closing with unsaved edits. ViewerApp routes to the editor, the not-found list or the choose list. Preflight rulings carried: one reload helper that passes the truncated flag and polled hash (P3/P14), a poll right after a save conflict so Overwrite on save adopts the current hash (P4), a chunked base64 encoder (P5), StatusIndicator for the badge (P11), banner-only test queries (P2), and a Range geometry stub for jsdom (P17). A save the container user may not write is reported as read-only and keeps the buffer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
IMAGE_PREVIEW_LIMIT,
|
||||
TEXT_PREVIEW_LIMIT,
|
||||
decodeBase64,
|
||||
encodeBase64,
|
||||
extensionOf,
|
||||
imageMimeFor,
|
||||
looksBinary,
|
||||
@@ -76,3 +77,22 @@ describe("decodeBase64 / looksBinary", () => {
|
||||
expect(looksBinary(bytes)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeBase64", () => {
|
||||
it("matches btoa on a small input", () => {
|
||||
expect(encodeBase64(new Uint8Array([0xff, 0xd8, 0x00, 0x41]))).toBe(btoa("\xff\xd8\x00\x41"));
|
||||
});
|
||||
|
||||
it("round-trips 1 MiB without overflowing the call stack", () => {
|
||||
// Spreading a 1 MiB array into String.fromCharCode throws RangeError in V8.
|
||||
const bytes = new Uint8Array(TEXT_PREVIEW_LIMIT);
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 31 + 7) & 0xff;
|
||||
const back = decodeBase64(encodeBase64(bytes));
|
||||
expect(back.length).toBe(bytes.length);
|
||||
expect(back.every((b, i) => b === bytes[i])).toBe(true);
|
||||
});
|
||||
|
||||
it("encodes an empty input as the empty string", () => {
|
||||
expect(encodeBase64(new Uint8Array(0))).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,6 +96,19 @@ export function decodeBase64(base64: string): Uint8Array<ArrayBuffer> {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bytes → base64. Built 32 KiB at a time: spreading a whole buffer into
|
||||
* `String.fromCharCode` overflows the argument limit well below 1 MiB.
|
||||
*/
|
||||
export function encodeBase64(bytes: Uint8Array): string {
|
||||
const CHUNK = 0x8000;
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i += CHUNK) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* The classic heuristic: a NUL byte early on means this is not text. Cheap,
|
||||
* and it is what `git` and `grep` use to decide the same question.
|
||||
|
||||
Reference in New Issue
Block a user