Add GitHub-style code preview + UE icon in context menu
- New CodeView: syntax-highlighted source (highlight.js, bundled offline) with line numbers for new files, colored unified diff for edits. Covers cpp/c/h, cs, py, js/ts, json/uproject, xml, ini, glsl. hljs tokens mapped to the app palette so it's theme-aware. Replaces the bare DiffView. - Context-menu items now support icons; the Launch Unreal Engine item shows the UE mark. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
120
src/CodeView.tsx
Normal file
120
src/CodeView.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { useEffect, useMemo, 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<string, string> = { 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.
|
||||
export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
const dp = file.depotFile || "";
|
||||
const { name } = splitPath(dp);
|
||||
const action = (file.action || "").toLowerCase();
|
||||
const [diff, setDiff] = useState<string | null>(null);
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
const [tooBig, setTooBig] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
setDiff(null); setCode(null); setErr(""); setTooBig(false);
|
||||
// edits show a diff; adds have none, so fetch it but don't depend on it
|
||||
p4.diff(dp).then((d) => live && setDiff(d)).catch(() => live && setDiff(""));
|
||||
p4.readDepot(dp).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]);
|
||||
|
||||
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]);
|
||||
|
||||
if (err === "binary") return <div className="nofile">{t("Binary file — no code preview.")}</div>;
|
||||
if (err) return <div className="nofile" style={{ color: "var(--del)" }}>{err}</div>;
|
||||
if (tooBig) return <div className="nofile">{t("File is too large to preview.")}</div>;
|
||||
|
||||
// an edited file with a real diff → show the diff
|
||||
if (diff && diff.trim() && action.includes("edit")) {
|
||||
const lines = diff.split("\n");
|
||||
return (
|
||||
<div className="right-body">
|
||||
<div className="codebar"><span className="cb-badge diff">{t("Diff")}</span><span className="cb-name">{name}</span></div>
|
||||
<div className="diff">
|
||||
{lines.map((ln, i) => {
|
||||
let cls = "";
|
||||
if (ln.startsWith("@@")) cls = "hunk";
|
||||
else if (ln.startsWith("+") && !ln.startsWith("+++")) cls = "add";
|
||||
else if (ln.startsWith("-") && !ln.startsWith("---")) cls = "del";
|
||||
return <div key={i} className={"dl " + cls}><span className="g" /><span className="c">{ln || " "}</span></div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// otherwise (new file, or edit with no text diff) → full content with line numbers
|
||||
if (code == null) return <div className="nofile">{t("Loading…")}</div>;
|
||||
const total = code.length ? code.replace(/\n$/, "").split("\n").length : 0;
|
||||
return (
|
||||
<div className="right-body">
|
||||
<div className="codebar">
|
||||
<span className={"cb-badge" + (action.includes("add") ? " add" : "")}>{action.includes("add") ? t("New file") : lang ? (LANG_LABEL[lang] || "") : t("Text")}</span>
|
||||
<span className="cb-name">{name}</span>
|
||||
<span className="cb-lines">{t("{n} lines", { n: total })}</span>
|
||||
</div>
|
||||
<div className={"code" + (action.includes("add") ? " added" : "")}>
|
||||
<div className="code-gutter">{Array.from({ length: total }, (_, i) => <span key={i}>{i + 1}</span>)}</div>
|
||||
<pre className="code-body"><code className="hljs" dangerouslySetInnerHTML={{ __html: highlighted }} /></pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user