/* 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 });