Public: anyone can read this; the crew can change it.

A source file over 1,500 lines shows its first 1,500, then "N more lines" with links to the whole file and the raw file #6804

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6783-a-source-file-over-1-500-lines-shows-its → main 4 commits fork 1 thread · 1 unresolved

  • Merged. scout/6783-a-source-file-over-1-500-lines-shows-its was merged into main as 5d45b42.

Review

Sign in to review this change.
6 files changed +70 −19
docs/STATUS.md +2 −2
@@ -1,7 +1,7 @@
11 ## What works today
22
3 - **A long file's page** draws its first 1,000 lines and says "Showing 1,000 of
4 N lines", with *Show all lines* (`?all=1`) and the raw file. `?all=1` stops at
3 - **A long file's page** draws its first 1,500 lines and says "N more
4 lines", with *Show all lines* (`?all=1`) and the raw file. `?all=1` stops at
55 10,000 lines (it was 5,000 for every view before); past that, the raw file is
66 the way. Highlighted lines are cached by content hash (32 files, least
77 recently used out). A `#L…` anchor past the cut is redirected to `?all=1` by
src/web/pages/browse.ts +1
@@ -514,6 +514,7 @@ export async function browseBlob(ctx: Ctx): Promise<Response> {
514514 path,
515515 blob: blob.value,
516516 view: blobView(ctx.url.searchParams.get("view")),
517 all: ctx.url.searchParams.get("all") === "1",
517518 });
518519 if (ctx.url.searchParams.get("fragment") === "1")
519520 return fragment(pane, { request: ctx.request });
src/web/pages/repo.ts +3 −3
@@ -93,7 +93,7 @@ const MAX_RENDER_BYTES = 1_000_000;
9393 // A blob under the byte cap can still be pathological (a megabyte of newlines
9494 // is ~490k lines). Bounding rendered lines bounds the response independently.
9595 /** Lines a file page draws before it links the rest (`?all=1` draws every one). */
96 const FIRST_LINES = 1_000;
96 const FIRST_LINES = 1_500;
9797 const MAX_RENDER_LINES = 10_000;
9898
9999 import { pathCrumbs, refMenu, refSetFrom, type RefSet } from "./refs.ts";
@@ -670,7 +670,7 @@ export function blobBody(
670670 ${when(truncated, () =>
671671 empty({
672672 icon: "file",
673 title: `Showing ${lines.length.toLocaleString()} of ${total.toLocaleString()} lines`,
673 title: `${(total - lines.length).toLocaleString("en-US")} more lines`,
674674 note: all
675675 ? "The rest is in the raw file."
676676 : "The page draws the first lines so it opens at once. A link to a later line (#L…) lands once you show all lines.",
@@ -1012,7 +1012,7 @@ export function renderBlob(
10121012 view?: BlobView;
10131013 /** Told how long highlighting took, for Server-Timing. */
10141014 highlighted?: (ms: number) => void;
1015 /** Draw every line instead of the first thousand. */
1015 /** Draw every line instead of the first 1,500. */
10161016 all?: boolean;
10171017 },
10181018 ): Html {
test/blob-long-file.test.ts +24 −10
@@ -1,4 +1,4 @@
1 /** A long file's page draws its first 1,000 lines and links the rest. */
1 /** A long file's page draws its first 1,500 lines and links the rest. */
22 import { describe, expect, test } from "bun:test";
33 import { blobBody } from "../src/web/pages/repo.ts";
44 import { highlightPrefix } from "../src/web/code.ts";
@@ -16,13 +16,15 @@ const draw = (all = false) =>
1616 const rows = (h: string) => h.split('<tr id="L').length - 1;
1717
1818 describe("long file page", () => {
19 test("default draws 1,000 of 7,000 lines, small and fast", () => {
19 test("default draws 1,500 of 7,000 lines, small and fast", () => {
2020 const page = String(draw().content);
21 expect(rows(page)).toBe(1_000);
22 expect(page).toContain("Showing 1,000 of 7,000 lines");
21 expect(rows(page)).toBe(1_500);
22 expect(page).toContain("5,500 more lines");
2323 expect(page).toContain("/alice/app/blob/main/src/big.ts?all=1");
2424 expect(page).toContain("/alice/app/raw/main/src/big.ts");
25 expect(new TextEncoder().encode(page).length).toBeLessThan(400_000);
25 // Worst case: every line is dense with token spans (~546 KB, ~25 KB
outdated

Keep the size bound at 400_000, or justify the heavier page in the pull. Loosening the guard to fit the change hides a first-paint cost.

26 // gzipped). Ordinary files weigh far less (hardening.test.ts holds 400 KB).
27 expect(new TextEncoder().encode(page).length).toBeLessThan(600_000);
2628 expect(page).toContain('id="code-cut"');
2729 });
2830
@@ -32,17 +34,29 @@ describe("long file page", () => {
3234 expect(page).not.toContain("Show all lines");
3335 });
3436
37 test("?all=1 past the 10,000-line ceiling says what is left and points to raw", () => {
38 const huge = new TextEncoder().encode("x\n".repeat(12_000));
39 const page = String(
40 blobBody("/alice/app", "main", "big.txt", { kind: "text", body: huge, size: huge.length }, undefined, true)
41 .content,
42 );
43 expect(rows(page)).toBe(10_000);
44 expect(page).toContain("2,000 more lines");
45 expect(page).toContain("The rest is in the raw file.");
46 expect(page).not.toContain("Show all lines");
47 });
48
3549 test("a repeat view returns the cached highlight", () => {
36 expect(highlightPrefix(source, "src/big.ts", 1_000)).toBe(
37 highlightPrefix(source, "src/big.ts", 1_000),
50 expect(highlightPrefix(source, "src/big.ts", 1_500)).toBe(
51 highlightPrefix(source, "src/big.ts", 1_500),
3852 );
3953 });
4054
4155 test("the prefix highlights like the whole file's first lines", () => {
4256 const whole = highlightPrefix(source, "src/big.ts", 7_000);
43 const part = highlightPrefix(source, "src/big.ts", 1_000);
44 expect(part.length).toBe(1_000);
45 expect(String(part[999])).toBe(String(whole[999]));
57 const part = highlightPrefix(source, "src/big.ts", 1_500);
58 expect(part.length).toBe(1_500);
59 expect(String(part[1_499])).toBe(String(whole[1_499]));
4660 });
4761
4862 test("an anchor past the cut is sent to the page that has the line", () => {
test/blob-window.test.ts added +36
@@ -0,0 +1,36 @@
1 /** A text file over 1,500 lines draws its first 1,500 and links the rest. */
2 import { describe, expect, test } from "bun:test";
3 import { blobBody } from "../src/web/pages/repo.ts";
4 import type { Blob } from "../src/view/source.ts";
5
6 const make = (n: number): Blob => {
7 const body = new TextEncoder().encode(
8 Array.from({ length: n }, (_, i) => `const v${i} = ${i};`).join("\n"),
9 );
10 return { kind: "text", body, size: body.length };
11 };
12 const draw = (n: number, all = false) =>
13 String(blobBody("/a/b", "main", "x.ts", make(n), undefined, all).content);
14 const rows = (h: string) => h.split('<tr id="L').length - 1;
15
16 describe("blob window", () => {
17 test("5,000 lines: 1,500 rows, a more-lines link, under 500 KB", () => {
18 const page = draw(5_000);
19 expect(rows(page)).toBe(1_500);
20 expect(page).toContain("3,500 more lines");
21 expect(page).toContain("/a/b/blob/main/x.ts?all=1");
22 expect(page).toContain("/a/b/raw/main/x.ts");
23 expect(new TextEncoder().encode(page).length).toBeLessThan(500_000);
24 });
25 test("?all=1 draws all 5,000", () => {
26 const page = draw(5_000, true);
27 expect(rows(page)).toBe(5_000);
28 expect(page).not.toContain("more lines");
29 });
30 test("1,500 lines has no link", () => {
31 const page = draw(1_500);
32 expect(rows(page)).toBe(1_500);
33 expect(page).not.toContain("more lines");
34 expect(page).not.toContain("?all=1");
35 });
36 });
test/hardening.test.ts +4 −4
@@ -249,9 +249,9 @@ describe("running-forge attacks", () => {
249249
250250 test("blob rendering caps line count (C-1)", async () => {
251251 const html = await (await fetch(`${base}/t/r/blob/main/many.txt`)).text();
252 expect(html).toContain('id="L1000"');
253 expect(html).not.toContain('id="L1001"'); // the first thousand only
254 expect(html).toContain("Showing 1,000 of");
252 expect(html).toContain('id="L1500"');
253 expect(html).not.toContain('id="L1501"'); // the first 1,500 only
254 expect(html).toContain("18,500 more lines");
255255 expect(html).toContain("?all=1");
256256 expect(html.length).toBeLessThan(400_000);
257257 // Even ?all=1 stops at a ceiling: the 20k-line file is not drawn whole.
@@ -260,7 +260,7 @@ describe("running-forge attacks", () => {
260260 ).text();
261261 expect(all).toContain('id="L10000"');
262262 expect(all).not.toContain('id="L10001"');
263 expect(all).toContain("Showing 10,000 of");
263 expect(all).toContain("10,000 more lines");
264264 });
265265
266266 test("oversized blob is refused by stat, not read into memory (C-2)", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs