/* Editor: tabs + four view modes (Original / Updated / Diff / Split) + line selection */
function climbToLine(node) {
let el = node && node.nodeType === 3 ? node.parentElement : node;
while (el && !(el.dataset && el.dataset.line)) el = el.parentElement;
return el || null;
}
function EditorTabs({ tabs, active, onActivate, onClose }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current && ref.current.querySelector(".tab.active");
if (el) el.scrollIntoView({ block: "nearest", inline: "nearest" });
}, [active]);
return (
{tabs.map((t) => {
const name = t.path.split("/").pop();
return (
onActivate(t.path)}
onAuxClick={(e) => { if (e.button === 1) { e.preventDefault(); onClose(t.path); } }}
title={t.path}>
{name}
{t.changed && {t.modeLabel}}
{ e.stopPropagation(); onClose(t.path); }}>
{Icon.close({})}
);
})}
);
}
/* Generic pane: renders an array of line descriptors with selection + caret + context. */
function PaneView({ cacheKey, path, lines, lang, showSign, refLine, cursor, selection, setCursor, setSelection, onContext }) {
const anchorRef = useRef(null);
const html = useMemo(() => lines.map((l) => HL.hlLine(l.text, lang)), [cacheKey]);
function gutterClick(e, no) {
if (no == null) return;
e.stopPropagation();
if (e.shiftKey && anchorRef.current != null) {
const a = anchorRef.current;
setSelection({ path, start: Math.min(a, no), end: Math.max(a, no), anchor: a });
} else {
anchorRef.current = no;
setSelection({ path, start: no, end: no, anchor: no });
}
setCursor({ path, line: no, col: 1 });
}
function caretCol(sel) {
try {
const el = climbToLine(sel.focusNode);
const code = el.querySelector(".ln-code");
const r = document.createRange();
r.setStart(code, 0); r.setEnd(sel.focusNode, sel.focusOffset);
return r.toString().length + 1;
} catch (e) { return 1; }
}
function onMouseUp() {
const sel = window.getSelection();
if (sel && !sel.isCollapsed) {
const a = climbToLine(sel.anchorNode), f = climbToLine(sel.focusNode);
if (a && f) {
const an = +a.dataset.line, fn = +f.dataset.line;
const s = Math.min(an, fn), e = Math.max(an, fn);
if (s !== e) { setSelection({ path, start: s, end: e, anchor: an }); setCursor({ path, line: fn, col: caretCol(sel) }); return; }
}
}
if (sel && sel.focusNode) {
const el = climbToLine(sel.focusNode);
if (el) { setCursor({ path, line: +el.dataset.line, col: caretCol(sel) }); setSelection(null); }
}
}
function handleContext(e) {
e.preventDefault();
const sel = window.getSelection();
let info = { path, kind: "editor" };
const a = sel && sel.anchorNode && climbToLine(sel.anchorNode);
const f = sel && sel.focusNode && climbToLine(sel.focusNode);
if (sel && !sel.isCollapsed && a && f && +a.dataset.line !== +f.dataset.line) {
const s = Math.min(+a.dataset.line, +f.dataset.line), en = Math.max(+a.dataset.line, +f.dataset.line);
info.sel = { start: s, end: en }; info.line = s;
} else if (selection && selection.path === path && selection.start !== selection.end) {
info.sel = { start: selection.start, end: selection.end }; info.line = selection.start;
} else {
let no = null;
const r = document.caretRangeFromPoint ? document.caretRangeFromPoint(e.clientX, e.clientY) : null;
const el = r && climbToLine(r.startContainer);
if (el) no = +el.dataset.line;
info.line = no || (cursor && cursor.path === path ? cursor.line : 1);
}
setCursor({ path, line: info.line, col: 1 });
onContext(e, info);
}
const curLine = cursor && cursor.path === path ? cursor.line : -1;
const sel = selection && selection.path === path ? selection : null;
return (
{lines.map((l, i) => {
const no = l.no;
const inSel = sel && no != null && no >= sel.start && no <= sel.end;
const cls = "ln-row"
+ (l.row === "add" ? " add" : l.row === "del" ? " del" : "")
+ (l.row === "bar-add" ? " bar-add" : l.row === "bar-del" ? " bar-del" : "")
+ (no === curLine && !inSel && !l.row ? " cursor" : "")
+ (inSel ? " selrange" : "");
return (
gutterClick(e, no)}>{no == null ? "" : no}
{showSign && {l.sign === " " || !l.sign ? "" : l.sign}}
);
})}
);
}
/* Build the line descriptors for a given mode. */
function buildLines(mode, diff, fileText) {
if (mode === "original") return { lines: diff.left.map((l) => ({ no: l.no, text: l.text, row: l.mark === "del" ? "bar-del" : null })), showSign: false };
if (mode === "updated") return { lines: diff.right.map((l) => ({ no: l.no, text: l.text, row: l.mark === "add" ? "bar-add" : null })), showSign: false };
if (mode === "diff") return { lines: diff.rows.map((r) => ({ no: r.newNo || r.oldNo, text: r.text, sign: r.sign, row: r.sign === "+" ? "add" : r.sign === "-" ? "del" : null })), showSign: true };
// plain file
const arr = (fileText || "").replace(/\n$/, "").split("\n");
return { lines: arr.map((t, i) => ({ no: i + 1, text: t })), showSign: false };
}
const SEGMENTS = [
{ id: "original", label: "Original" },
{ id: "updated", label: "Updated" },
{ id: "diff", label: "Diff" },
];
function Editor({ tabs, active, mode, setMode, onActivate, onClose, onContext, onSplit, splitOpen, cursor, selection, setCursor, setSelection }) {
const tab = tabs.find((t) => t.path === active);
const change = tab ? PROJECT.changes.find((c) => c.path === tab.path) : null;
const diff = tab ? PROJECT.diffs[tab.path] : null;
const lang = tab ? HL.langFor(tab.path) : null;
const effMode = change ? mode : "code";
let built = null;
if (tab) {
if (change && diff) built = buildLines(effMode, diff, PROJECT.files[tab.path]);
else built = buildLines("code", null, PROJECT.files[tab.path]);
}
const statusWord = change ? (change.status === "A" ? "Added" : change.status === "D" ? "Deleted" : "Modified") : "";
const activeSeg = splitOpen ? "split" : effMode;
const emptyUpdated = effMode === "updated" && built && built.lines.length === 0;
const emptyOriginal = effMode === "original" && built && built.lines.length === 0;
return (
{!tab ? (
{Icon.file({ width: 30, height: 30 })}
No file open
Search files & content⌘ F
Copy referenceright-click
Pass on to Agentright-click
) : (
{change && (
{statusWord}
{change.add > 0 &&
+{change.add}}
{change.del > 0 &&
−{change.del}}
{SEGMENTS.map((s) => (
))}
)}
{emptyUpdated ? (
No updated version
This file was deleted in the change.
) : emptyOriginal ? (
No original version
This file is new in the change.
) : (
)}
)}
);
}
/* Full-screen side-by-side split view */
function SplitView({ path, onClose, onContext }) {
const diff = PROJECT.diffs[path];
const lang = HL.langFor(path);
const leftRef = useRef(null), rightRef = useRef(null);
const lock = useRef(false);
const change = PROJECT.changes.find((c) => c.path === path);
const leftHtml = useMemo(() => diff.split.map((r) => r.l ? HL.hlLine(r.l.text, lang) : ""), [path]);
const rightHtml = useMemo(() => diff.split.map((r) => r.r ? HL.hlLine(r.r.text, lang) : ""), [path]);
function sync(from, to) {
if (lock.current) return; lock.current = true;
to.scrollTop = from.scrollTop; to.scrollLeft = from.scrollLeft;
requestAnimationFrame(() => { lock.current = false; });
}
function ctx(e, side) {
e.preventDefault();
let no = null;
const r = document.caretRangeFromPoint ? document.caretRangeFromPoint(e.clientX, e.clientY) : null;
const el = r && climbToLine(r.startContainer);
if (el) no = +el.dataset.line;
onContext(e, { path, kind: "editor", line: no || 1 });
}
return (
{path}
{change &&
{change.status === "A" ? "Added" : change.status === "D" ? "Deleted" : "Modified"}}
{change && change.add > 0 &&
+{change.add}}
{change && change.del > 0 &&
−{change.del}}
Original before
sync(leftRef.current, rightRef.current)} onContextMenu={(e) => ctx(e, "l")}>
{diff.split.map((row, i) => (
{row.l ? row.l.no : ""}
))}
Updated after
sync(rightRef.current, leftRef.current)} onContextMenu={(e) => ctx(e, "r")}>
{diff.split.map((row, i) => (
{row.r ? row.r.no : ""}
))}
);
}
Object.assign(window, { Editor, EditorTabs, PaneView, SplitView, buildLines, climbToLine });