import { useEffect, useMemo, useRef, useState } from "react"; import hljs from "highlight.js/lib/core"; import cpp from "highlight.js/lib/languages/cpp"; import csharp from "highlight.js/lib/languages/csharp"; import python from "highlight.js/lib/languages/python"; import javascript from "highlight.js/lib/languages/javascript"; import typescript from "highlight.js/lib/languages/typescript"; import json from "highlight.js/lib/languages/json"; import xml from "highlight.js/lib/languages/xml"; import ini from "highlight.js/lib/languages/ini"; import glsl from "highlight.js/lib/languages/glsl"; import { p4, OpenedFile, splitPath } from "./p4"; import { t } from "./i18n"; hljs.registerLanguage("cpp", cpp); hljs.registerLanguage("csharp", csharp); hljs.registerLanguage("python", python); hljs.registerLanguage("javascript", javascript); hljs.registerLanguage("typescript", typescript); hljs.registerLanguage("json", json); hljs.registerLanguage("xml", xml); hljs.registerLanguage("ini", ini); hljs.registerLanguage("glsl", glsl); // file extension -> highlight.js language (empty = no highlighting, plain text) function langOf(name: string): string { const e = (name.split(".").pop() || "").toLowerCase(); if (/^(cpp|cc|cxx|c\+\+|hpp|hxx|h|hh|inl|c|ino)$/.test(e)) return "cpp"; if (e === "cs") return "csharp"; if (e === "py") return "python"; if (/^(js|jsx|mjs|cjs)$/.test(e)) return "javascript"; if (/^(ts|tsx)$/.test(e)) return "typescript"; if (/^(json|uproject|uplugin)$/.test(e)) return "json"; if (/^(xml|html|htm|svg|xaml)$/.test(e)) return "xml"; if (/^(ini|cfg|conf|config|toml|editorconfig)$/.test(e)) return "ini"; if (/^(usf|ush|hlsl|glsl|shader|frag|vert)$/.test(e)) return "glsl"; return ""; } const LANG_LABEL: Record = { cpp: "C++", csharp: "C#", python: "Python", javascript: "JavaScript", typescript: "TypeScript", json: "JSON", xml: "XML", ini: "INI", glsl: "GLSL" }; function esc(s: string): string { return s.replace(/[&<>]/g, (c) => (c === "&" ? "&" : c === "<" ? "<" : ">")); } const MAX = 800_000; // don't try to render/highlight enormous files // Preview source files like GitHub: syntax-highlighted content with line numbers // for new files, or a colored unified diff for edits. Working-copy code files can // be edited in place (built-in editor) and saved before committing. export default function CodeView({ file, onFlash }: { file: OpenedFile; onFlash?: (text: string, err?: boolean) => void }) { const dp = file.depotFile || ""; const spec = file._spec || ""; // set → read this exact submitted revision const { name } = splitPath(dp); const action = (file.action || "").toLowerCase(); const [diff, setDiff] = useState(null); const [code, setCode] = useState(null); const [err, setErr] = useState(""); const [tooBig, setTooBig] = useState(false); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); const [saving, setSaving] = useState(false); const [reload, setReload] = useState(0); useEffect(() => { let live = true; setDiff(null); setCode(null); setErr(""); setTooBig(false); setEditing(false); // working-copy edits show a diff against the depot; historical revisions just // show their full content (no meaningful working diff to fetch). if (!spec) p4.diff(dp).then((d) => live && setDiff(d)).catch(() => live && setDiff("")); const load = spec ? p4.printDepot(spec) : p4.readDepot(dp); load.then((buf) => { if (!live) return; if (buf.byteLength > MAX) { setTooBig(true); setCode(""); return; } const bytes = new Uint8Array(buf); // crude binary sniff: a NUL in the first chunk if (bytes.subarray(0, 8000).includes(0)) { setCode(null); setErr("binary"); return; } setCode(new TextDecoder("utf-8", { fatal: false }).decode(bytes)); }).catch((e) => live && setErr(String(e))); return () => { live = false; }; }, [dp, spec, reload]); // built-in editor: editable for a code file in the working copy (not history/binary/huge) const canEdit = !spec && !tooBig && err !== "binary" && code != null; function startEdit() { setDraft(code || ""); setEditing(true); } function cancelEdit() { setEditing(false); } async function save() { setSaving(true); try { await p4.writeDepot(dp, draft); setCode(draft); setEditing(false); onFlash?.(t("Saved {name}", { name })); setReload((r) => r + 1); // refresh diff/status after the on-disk change } catch (e) { onFlash?.(String(e), true); } finally { setSaving(false); } } const pencil = ; // NB: all hooks must run before any early return (Rules of Hooks) — compute // the highlighted body here, above the editing/diff/error returns below. const lang = langOf(name); const highlighted = useMemo(() => { if (code == null) return ""; try { return lang ? hljs.highlight(code, { language: lang, ignoreIllegals: true }).value : esc(code); } catch { return esc(code); } }, [code, lang]); // live-highlighted draft for the editor underlay (same highlighter as the preview) const draftHi = useMemo(() => { try { return lang ? hljs.highlight(draft, { language: lang, ignoreIllegals: true }).value : esc(draft); } catch { return esc(draft); } }, [draft, lang]); const preRef = useRef(null); const gutRef = useRef(null); const draftRows = draft.split("\n").length; // editing mode — a real code editor: transparent textarea over a syntax- // highlighted underlay, with a line-number gutter (looks like the preview). if (editing) { const syncScroll = (e: { currentTarget: HTMLTextAreaElement }) => { const ta = e.currentTarget; if (preRef.current) { preRef.current.scrollTop = ta.scrollTop; preRef.current.scrollLeft = ta.scrollLeft; } if (gutRef.current) gutRef.current.scrollTop = ta.scrollTop; }; return (
{t("Editing")} {name} {lang ? {LANG_LABEL[lang]} : null}
{Array.from({ length: draftRows }, (_, i) => {i + 1})}