From 7a96efd290b45f8c4210c0f7cf0f1e9a35c08b8c Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Wed, 25 Feb 2026 13:10:11 -0800 Subject: [PATCH] =?UTF-8?q?Revert=20"Revert=20"CodeBlock:=20Virtualize=20s?= =?UTF-8?q?yntax=20highlighting=20for=20long=20code=20submi=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7ff0a6abbae62762916b856f3d416efd55ed3ca7. --- frontend/package-lock.json | 69 +++++---- frontend/package.json | 2 + .../components/codeblock/CodeBlock.test.tsx | 84 ++++++++++- .../src/components/codeblock/CodeBlock.tsx | 133 +++++++++++++++++- frontend/src/lib/hooks/useContainerHeight.ts | 35 +++++ .../components/SubmissionCodeSidebar.tsx | 5 +- 6 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 frontend/src/lib/hooks/useContainerHeight.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bfac60a..4d3fa6c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", "react-syntax-highlighter": "^16.1.0", + "react-window": "^1.8.11", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", @@ -40,6 +41,7 @@ "@types/node": "^24.0.3", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", + "@types/react-window": "^1.8.8", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", @@ -118,7 +120,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -444,7 +445,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -467,7 +467,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } @@ -524,7 +523,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -565,7 +563,6 @@ "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1296,7 +1293,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6", "@mui/core-downloads-tracker": "^7.2.0", @@ -1897,7 +1893,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2021,7 +2018,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -2046,7 +2042,6 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2056,7 +2051,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2078,6 +2072,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2126,7 +2130,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", @@ -2478,7 +2481,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2525,6 +2527,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -2638,7 +2641,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2995,7 +2997,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -3010,7 +3013,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", - "peer": true, "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" @@ -3126,7 +3128,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4065,7 +4066,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -4271,6 +4271,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4588,6 +4589,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5471,6 +5478,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5485,6 +5493,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5496,7 +5505,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/prismjs": { "version": "1.30.0", @@ -5564,7 +5574,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5573,7 +5582,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5702,6 +5710,23 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-window": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -6226,7 +6251,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -6367,7 +6391,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6597,7 +6620,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6708,7 +6730,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index d547712..161f637 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", "react-syntax-highlighter": "^16.1.0", + "react-window": "^1.8.11", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", @@ -44,6 +45,7 @@ "@types/node": "^24.0.3", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", + "@types/react-window": "^1.8.8", "@vitejs/plugin-react": "^4.5.2", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/frontend/src/components/codeblock/CodeBlock.test.tsx b/frontend/src/components/codeblock/CodeBlock.test.tsx index 0a71d78..777cbe0 100644 --- a/frontend/src/components/codeblock/CodeBlock.test.tsx +++ b/frontend/src/components/codeblock/CodeBlock.test.tsx @@ -1,5 +1,5 @@ -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import CodeBlock from "./CodeBlock"; // Mock clipboard API @@ -9,6 +9,30 @@ Object.assign(navigator, { }, }); +// Mock ResizeObserver (jsdom doesn't implement it) +beforeEach(() => { + window.ResizeObserver = vi.fn().mockImplementation((callback) => ({ + observe: vi.fn((element: Element) => { + // Simulate measurement on next microtask + Promise.resolve().then(() => + callback( + [{ target: element, contentRect: { height: 600, width: 800 } }], + {} as ResizeObserver, + ), + ); + }), + unobserve: vi.fn(), + disconnect: vi.fn(), + })) as unknown as typeof ResizeObserver; +}); + +function generateLongCode(lines: number): string { + return Array.from( + { length: lines }, + (_, i) => `x = ${i} # line ${i + 1}`, + ).join("\n"); +} + describe("CodeBlock", () => { const sampleCode = `function hello() { console.log("Hello, world!"); @@ -50,4 +74,60 @@ describe("CodeBlock", () => { render(); expect(screen.getByText(/Hello/)).toBeInTheDocument(); }); + + describe("virtualization", () => { + it("does not virtualize short code", async () => { + render(); + // Wait for rAF + await act(async () => { + await new Promise((r) => setTimeout(r, 50)); + }); + // Short code should use a
 or standard SyntaxHighlighter, not FixedSizeList
+      expect(document.querySelector("pre")).toBeInTheDocument();
+    });
+
+    it("virtualizes code with more than 200 lines", async () => {
+      const longCode = generateLongCode(300);
+      render();
+
+      // Wait for rAF + ResizeObserver
+      await act(async () => {
+        await new Promise((r) => setTimeout(r, 50));
+      });
+
+      // FixedSizeList renders a container with explicit height
+      await waitFor(() => {
+        const listContainer = document.querySelector(
+          '[style*="height: 600px"]',
+        );
+        expect(listContainer).toBeInTheDocument();
+      });
+    });
+
+    it("does not virtualize when bordered is true", async () => {
+      const longCode = generateLongCode(300);
+      render();
+
+      await act(async () => {
+        await new Promise((r) => setTimeout(r, 50));
+      });
+
+      // Should not have a FixedSizeList container
+      const listContainer = document.querySelector(
+        '[style*="height: 600px"]',
+      );
+      expect(listContainer).not.toBeInTheDocument();
+    });
+
+    it("copy button works with virtualized code", async () => {
+      const longCode = generateLongCode(300);
+      const writeTextSpy = vi.spyOn(navigator.clipboard, "writeText");
+      render();
+
+      const copyButton = screen.getByRole("button");
+      fireEvent.click(copyButton);
+
+      expect(writeTextSpy).toHaveBeenCalledWith(longCode);
+    });
+  });
 });
diff --git a/frontend/src/components/codeblock/CodeBlock.tsx b/frontend/src/components/codeblock/CodeBlock.tsx
index 16d8755..4c467f7 100644
--- a/frontend/src/components/codeblock/CodeBlock.tsx
+++ b/frontend/src/components/codeblock/CodeBlock.tsx
@@ -1,9 +1,50 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
 import { Box, IconButton, Tooltip, useTheme } from "@mui/material";
 import ContentCopyIcon from "@mui/icons-material/ContentCopy";
 import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
 import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
 import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
+import { FixedSizeList } from "react-window";
+import { useContainerHeight } from "../../lib/hooks/useContainerHeight";
+import type { rendererNode } from "react-syntax-highlighter";
+
+function renderNode(
+  node: rendererNode,
+  stylesheet: { [key: string]: React.CSSProperties },
+  useInlineStyles: boolean,
+  key: React.Key,
+): React.ReactNode {
+  if (node.type === "text") return node.value;
+  if (!node.tagName) return null;
+
+  const classNames = node.properties?.className ?? [];
+  let props: Record;
+  if (useInlineStyles) {
+    const nonToken = classNames.filter((c: string) => c !== "token");
+    const style: React.CSSProperties = {};
+    for (const cls of nonToken) {
+      if (stylesheet[cls]) Object.assign(style, stylesheet[cls]);
+    }
+    // Handle compound selectors (e.g. "keyword.control")
+    if (nonToken.length >= 2) {
+      const a = nonToken.join(".");
+      if (stylesheet[a]) Object.assign(style, stylesheet[a]);
+      const b = nonToken.slice().reverse().join(".");
+      if (stylesheet[b]) Object.assign(style, stylesheet[b]);
+    }
+    props = { key, style };
+  } else {
+    props = { key, className: classNames.join(" ") };
+  }
+
+  const children = node.children?.map((child, i) =>
+    renderNode(child, stylesheet, useInlineStyles, `${key}-${i}`),
+  );
+  return React.createElement(node.tagName, props, children);
+}
+
+const VIRTUALIZATION_LINE_THRESHOLD = 200;
+const LINE_HEIGHT_PX = 20;
 
 interface CodeBlockProps {
   code: string;
@@ -33,6 +74,7 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
   const [highlighted, setHighlighted] = useState(false);
   const theme = useTheme();
   const syntaxTheme = theme.palette.mode === "dark" ? oneDark : oneLight;
+  const { containerRef, height: containerHeight } = useContainerHeight();
 
   useEffect(() => {
     setHighlighted(false);
@@ -40,6 +82,18 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
     return () => cancelAnimationFrame(id);
   }, [code]);
 
+  const lineCount = useMemo(() => {
+    let count = 1;
+    for (let i = 0; i < code.length; i++) {
+      if (code[i] === "\n") count++;
+    }
+    return count;
+  }, [code]);
+
+  const canVirtualize =
+    !bordered && lineCount > VIRTUALIZATION_LINE_THRESHOLD;
+  const shouldVirtualize = canVirtualize && containerHeight > 0;
+
   const handleCopy = () => {
     navigator.clipboard.writeText(code).then(() => {
       setCopied(true);
@@ -47,8 +101,66 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo
     });
   };
 
+  const virtualizedRenderer = useMemo(() => {
+    if (!shouldVirtualize) return undefined;
+
+    return ({
+      rows,
+      stylesheet,
+      useInlineStyles,
+    }: {
+      rows: rendererNode[];
+      stylesheet: { [key: string]: React.CSSProperties };
+      useInlineStyles: boolean;
+    }): React.ReactNode => (
+      
+        {({
+          index,
+          style,
+        }: {
+          index: number;
+          style: React.CSSProperties;
+        }) => (
+          
+ {renderNode( + rows[index], + stylesheet, + useInlineStyles, + `line-${index}`, + )} +
+ )} +
+ ); + }, [shouldVirtualize, containerHeight]); + return ( - + {/* Copy Button */} @@ -60,8 +172,11 @@ export default React.memo(function CodeBlock({ code, bordered = false }: CodeBlo {/* Scrollable Code with Syntax Highlighting */} - {highlighted ? ( + {highlighted && (!canVirtualize || shouldVirtualize) ? ( {code} diff --git a/frontend/src/lib/hooks/useContainerHeight.ts b/frontend/src/lib/hooks/useContainerHeight.ts new file mode 100644 index 0000000..1f47c79 --- /dev/null +++ b/frontend/src/lib/hooks/useContainerHeight.ts @@ -0,0 +1,35 @@ +import { useRef, useState, useCallback, useEffect } from "react"; + +export function useContainerHeight(): { + containerRef: React.RefCallback; + height: number; +} { + const [height, setHeight] = useState(0); + const observerRef = useRef(null); + + const containerRef = useCallback((node: T | null) => { + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; + } + + if (node) { + observerRef.current = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + const newHeight = Math.floor(entry.contentRect.height); + setHeight((prev) => (prev !== newHeight ? newHeight : prev)); + } + }); + observerRef.current.observe(node); + } + }, []); + + useEffect(() => { + return () => { + observerRef.current?.disconnect(); + }; + }, []); + + return { containerRef, height }; +} diff --git a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx index cfa550b..77a44a1 100644 --- a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx +++ b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx @@ -273,8 +273,11 @@ export default function SubmissionCodeSidebar({ {isLoadingCodes ? (