/* Editor: tabs + four view modes (Original / Updated / Diff / Split) + line selection */
const { ref, computed, onMounted, nextTick, watch } = Vue;
/* ---------- framework-agnostic helpers (ported verbatim) ---------- */
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 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" },
];
/* ---------- EditorTabs ---------- */
const EditorTabs = {
props: ["tabs", "active", "onActivate", "onClose"],
setup(props) {
const rootRef = ref(null);
watch(() => props.active, async () => {
await nextTick();
const el = rootRef.value && rootRef.value.querySelector(".tab.active");
if (el) el.scrollIntoView({ block: "nearest", inline: "nearest" });
});
return { rootRef };
},
template: `
{ if (e.button === 1) { e.preventDefault(); onClose(t.path); } }"
:title="t.path"
>
{{ t.path.split('/').pop() }}
{{ t.modeLabel }}
`,
};
/* ---------- PaneView ---------- */
const PaneView = {
props: ["cacheKey", "path", "lines", "lang", "showSign", "refLine", "cursor", "selection", "setCursor", "setSelection", "onContext"],
setup(props) {
// anchor for shift-click range selection
const anchorRef = ref(null);
// highlighted HTML — recomputed when cacheKey changes
const html = computed(() => props.lines.map((l) => HL.hlLine(l.text, props.lang)));
function gutterClick(e, no) {
if (no == null) return;
e.stopPropagation();
if (e.shiftKey && anchorRef.value != null) {
const a = anchorRef.value;
props.setSelection({ path: props.path, start: Math.min(a, no), end: Math.max(a, no), anchor: a });
} else {
anchorRef.value = no;
props.setSelection({ path: props.path, start: no, end: no, anchor: no });
}
props.setCursor({ path: props.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) { props.setSelection({ path: props.path, start: s, end: e, anchor: an }); props.setCursor({ path: props.path, line: fn, col: caretCol(sel) }); return; }
}
}
if (sel && sel.focusNode) {
const el = climbToLine(sel.focusNode);
if (el) { props.setCursor({ path: props.path, line: +el.dataset.line, col: caretCol(sel) }); props.setSelection(null); }
}
}
function handleContext(e) {
e.preventDefault();
const sel = window.getSelection();
let info = { path: props.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 (props.selection && props.selection.path === props.path && props.selection.start !== props.selection.end) {
info.sel = { start: props.selection.start, end: props.selection.end }; info.line = props.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 || (props.cursor && props.cursor.path === props.path ? props.cursor.line : 1);
}
props.setCursor({ path: props.path, line: info.line, col: 1 });
props.onContext(e, info);
}
return { html, gutterClick, onMouseUp, handleContext };
},
template: `
gutterClick(e, l.no)">{{ l.no == null ? '' : l.no }}
{{ l.sign === ' ' || !l.sign ? '' : l.sign }}
`,
};
/* ---------- Editor ---------- */
const Editor = {
props: ["tabs", "active", "mode", "setMode", "onActivate", "onClose", "onContext", "onSplit", "splitOpen", "cursor", "selection", "setCursor", "setSelection"],
setup(props) {
const tab = computed(() => props.tabs.find((t) => t.path === props.active));
const change = computed(() => tab.value ? PROJECT.changes.find((c) => c.path === tab.value.path) : null);
const diff = computed(() => tab.value ? PROJECT.diffs[tab.value.path] : null);
const lang = computed(() => tab.value ? HL.langFor(tab.value.path) : null);
const effMode = computed(() => change.value ? props.mode : "code");
const built = computed(() => {
if (!tab.value) return null;
if (change.value && diff.value) return buildLines(effMode.value, diff.value, PROJECT.files[tab.value.path]);
return buildLines("code", null, PROJECT.files[tab.value.path]);
});
const statusWord = computed(() => {
if (!change.value) return "";
return change.value.status === "A" ? "Added" : change.value.status === "D" ? "Deleted" : "Modified";
});
const activeSeg = computed(() => props.splitOpen ? "split" : effMode.value);
const emptyUpdated = computed(() => effMode.value === "updated" && built.value && built.value.lines.length === 0);
const emptyOriginal = computed(() => effMode.value === "original" && built.value && built.value.lines.length === 0);
return { tab, change, diff, lang, effMode, built, statusWord, activeSeg, emptyUpdated, emptyOriginal, SEGMENTS };
},
template: `
No file open
Search files & content⌘ F
Copy referenceright-click
Pass on to Agentright-click
{{ statusWord }}
+{{ change.add }}
−{{ change.del }}
No updated version
This file was deleted in the change.
No original version
This file is new in the change.
`,
};
/* ---------- SplitView ---------- */
const SplitView = {
props: ["path", "onClose", "onContext"],
setup(props) {
const diff = computed(() => PROJECT.diffs[props.path]);
const lang = computed(() => HL.langFor(props.path));
const change = computed(() => PROJECT.changes.find((c) => c.path === props.path));
const leftRef = ref(null);
const rightRef = ref(null);
const lock = ref(false);
const leftHtml = computed(() => diff.value.split.map((r) => r.l ? HL.hlLine(r.l.text, lang.value) : ""));
const rightHtml = computed(() => diff.value.split.map((r) => r.r ? HL.hlLine(r.r.text, lang.value) : ""));
function sync(from, to) {
if (lock.value) return; lock.value = true;
to.scrollTop = from.scrollTop;
to.scrollLeft = from.scrollLeft;
requestAnimationFrame(() => { lock.value = 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;
props.onContext(e, { path: props.path, kind: "editor", line: no || 1 });
}
return { diff, lang, change, leftRef, rightRef, sync, ctx, leftHtml, rightHtml };
},
template: `
{{ path }}
{{ change.status === 'A' ? 'Added' : change.status === 'D' ? 'Deleted' : 'Modified' }}
+{{ change.add }}
−{{ change.del }}
Original before
ctx(e, 'l')">
{{ row.l ? row.l.no : '' }}
Updated after
ctx(e, 'r')">
{{ row.r ? row.r.no : '' }}
`,
};
/* ---------- global registration ---------- */
window.HelderComponents = Object.assign(window.HelderComponents || {}, { EditorTabs, PaneView, Editor, SplitView });
window.buildLines = buildLines;
window.climbToLine = climbToLine;