mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
234 lines
8.4 KiB
TypeScript
234 lines
8.4 KiB
TypeScript
import { type DiffLineClass, type DisplayDiffLine, diffLineClass, diffLineClassFromText, displayDiffLineText } from "./unified";
|
|
|
|
const MAX_INLINE_DIFF_CHARS = 4000;
|
|
const MAX_INLINE_DIFF_TOKENS = 500;
|
|
|
|
interface InlineDiffPart {
|
|
text: string;
|
|
changed: boolean;
|
|
}
|
|
|
|
interface InlineDiff {
|
|
added: InlineDiffPart[];
|
|
removed: InlineDiffPart[];
|
|
}
|
|
|
|
interface RenderDiffLine {
|
|
text: string;
|
|
className: DiffLineClass;
|
|
}
|
|
|
|
type ChangeLineClass = "added" | "removed";
|
|
|
|
export interface RenderDiffLinesOptions {
|
|
className?: string;
|
|
}
|
|
|
|
export function renderDisplayDiffLines(parent: HTMLElement, lines: DisplayDiffLine[], options: RenderDiffLinesOptions = {}): HTMLElement {
|
|
return renderDiffLines(
|
|
parent,
|
|
lines.map((line) => ({ text: line.text, className: diffLineClass(line) })),
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function renderRawDiffLines(parent: HTMLElement, diff: string, options: RenderDiffLinesOptions = {}): HTMLElement {
|
|
return renderDiffLines(
|
|
parent,
|
|
diff.split("\n").map((line) => ({ text: line, className: diffLineClassFromText(line) })),
|
|
options,
|
|
);
|
|
}
|
|
|
|
function renderDiffLines(parent: HTMLElement, lines: RenderDiffLine[], options: RenderDiffLinesOptions): HTMLElement {
|
|
const className = ["codex-panel-diff", options.className].filter(Boolean).join(" ");
|
|
const pre = parent.createEl("pre", { cls: className });
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
if (!line) continue;
|
|
const changeClass = changeLineClass(line.className);
|
|
if (!changeClass) {
|
|
renderLine(pre, line);
|
|
continue;
|
|
}
|
|
|
|
const firstRun = collectChangeRun(lines, index, changeClass);
|
|
const secondRun = collectChangeRun(lines, firstRun.endIndex, oppositeChangeLineClass(changeClass));
|
|
if (secondRun.lines.length > 0) {
|
|
renderChangeRuns(pre, firstRun.lines, secondRun.lines);
|
|
index = secondRun.endIndex - 1;
|
|
continue;
|
|
}
|
|
|
|
for (const runLine of firstRun.lines) {
|
|
renderLine(pre, runLine);
|
|
}
|
|
index = firstRun.endIndex - 1;
|
|
}
|
|
|
|
return pre;
|
|
}
|
|
|
|
function collectChangeRun(
|
|
lines: RenderDiffLine[],
|
|
startIndex: number,
|
|
className: ChangeLineClass,
|
|
): { lines: RenderDiffLine[]; endIndex: number } {
|
|
const run: RenderDiffLine[] = [];
|
|
let index = startIndex;
|
|
while (index < lines.length && lines[index]?.className === className) {
|
|
const line = lines[index];
|
|
if (line) run.push(line);
|
|
index += 1;
|
|
}
|
|
return { lines: run, endIndex: index };
|
|
}
|
|
|
|
function renderChangeRuns(parent: HTMLElement, firstRun: RenderDiffLine[], secondRun: RenderDiffLine[]): void {
|
|
const inlineDiffs = pairedInlineDiffs(firstRun, secondRun);
|
|
for (let index = 0; index < firstRun.length; index += 1) {
|
|
const line = firstRun[index];
|
|
if (!line) continue;
|
|
renderLine(parent, line, inlinePartsForLine(line.className, inlineDiffs[index] ?? null));
|
|
}
|
|
for (let index = 0; index < secondRun.length; index += 1) {
|
|
const line = secondRun[index];
|
|
if (!line) continue;
|
|
renderLine(parent, line, inlinePartsForLine(line.className, inlineDiffs[index] ?? null));
|
|
}
|
|
}
|
|
|
|
function pairedInlineDiffs(firstRun: RenderDiffLine[], secondRun: RenderDiffLine[]): (InlineDiff | null)[] {
|
|
const pairCount = Math.min(firstRun.length, secondRun.length);
|
|
const inlineDiffs: (InlineDiff | null)[] = [];
|
|
for (let index = 0; index < pairCount; index += 1) {
|
|
const firstLine = firstRun[index];
|
|
const secondLine = secondRun[index];
|
|
if (!firstLine || !secondLine) {
|
|
inlineDiffs.push(null);
|
|
} else {
|
|
inlineDiffs.push(inlineDiffForLines(firstLine, secondLine));
|
|
}
|
|
}
|
|
return inlineDiffs;
|
|
}
|
|
|
|
function inlineDiffForLines(firstLine: RenderDiffLine, secondLine: RenderDiffLine): InlineDiff | null {
|
|
const removedLine = firstLine.className === "removed" ? firstLine : secondLine;
|
|
const addedLine = firstLine.className === "added" ? firstLine : secondLine;
|
|
return inlineDiff(displayDiffLineText(removedLine.text, removedLine.className), displayDiffLineText(addedLine.text, addedLine.className));
|
|
}
|
|
|
|
function inlinePartsForLine(className: DiffLineClass, inlineDiff: InlineDiff | null): InlineDiffPart[] | null {
|
|
if (!inlineDiff) return null;
|
|
if (className === "removed") return inlineDiff.removed;
|
|
if (className === "added") return inlineDiff.added;
|
|
return null;
|
|
}
|
|
|
|
function changeLineClass(className: DiffLineClass): ChangeLineClass | null {
|
|
return className === "added" || className === "removed" ? className : null;
|
|
}
|
|
|
|
function oppositeChangeLineClass(className: ChangeLineClass): ChangeLineClass {
|
|
return className === "added" ? "removed" : "added";
|
|
}
|
|
|
|
function renderLine(parent: HTMLElement, line: RenderDiffLine, inlineParts: InlineDiffPart[] | null = null): void {
|
|
const lineEl = parent.createEl("span", { cls: diffLineClassName(line.className) });
|
|
if (!inlineParts) {
|
|
lineEl.textContent = displayDiffLineText(line.text, line.className);
|
|
return;
|
|
}
|
|
for (const part of inlineParts) {
|
|
if (part.changed) {
|
|
lineEl.createSpan({ cls: diffWordClassName(line.className), text: part.text });
|
|
} else {
|
|
lineEl.createSpan({ text: part.text });
|
|
}
|
|
}
|
|
}
|
|
|
|
function diffLineClassName(className: DiffLineClass): string {
|
|
if (className === "added") return "codex-panel-diff__line codex-panel-diff__line--added";
|
|
if (className === "context") return "codex-panel-diff__line codex-panel-diff__line--context";
|
|
if (className === "file") return "codex-panel-diff__line codex-panel-diff__line--file";
|
|
if (className === "hunk") return "codex-panel-diff__line codex-panel-diff__line--hunk";
|
|
return "codex-panel-diff__line codex-panel-diff__line--removed";
|
|
}
|
|
|
|
function diffWordClassName(className: DiffLineClass): string {
|
|
if (className === "added") return "codex-panel-diff__word codex-panel-diff__word--added";
|
|
if (className === "removed") return "codex-panel-diff__word codex-panel-diff__word--removed";
|
|
return "codex-panel-diff__word";
|
|
}
|
|
|
|
function inlineDiff(removedText: string, addedText: string): InlineDiff | null {
|
|
if (removedText.length + addedText.length > MAX_INLINE_DIFF_CHARS) return null;
|
|
|
|
const removedTokens = segmentWords(removedText);
|
|
const addedTokens = segmentWords(addedText);
|
|
if (removedTokens.length + addedTokens.length > MAX_INLINE_DIFF_TOKENS) return null;
|
|
|
|
const lengths = lcsLengths(removedTokens, addedTokens);
|
|
const removed: InlineDiffPart[] = [];
|
|
const added: InlineDiffPart[] = [];
|
|
let removedIndex = 0;
|
|
let addedIndex = 0;
|
|
|
|
while (removedIndex < removedTokens.length || addedIndex < addedTokens.length) {
|
|
const removedToken = removedTokens[removedIndex];
|
|
const addedToken = addedTokens[addedIndex];
|
|
if (removedToken !== undefined && addedToken !== undefined && removedToken === addedToken) {
|
|
removed.push({ text: removedToken, changed: false });
|
|
added.push({ text: addedToken, changed: false });
|
|
removedIndex += 1;
|
|
addedIndex += 1;
|
|
} else if (
|
|
addedToken !== undefined &&
|
|
(removedToken === undefined || (lengths[removedIndex]?.[addedIndex + 1] ?? 0) >= (lengths[removedIndex + 1]?.[addedIndex] ?? 0))
|
|
) {
|
|
added.push({ text: addedToken, changed: true });
|
|
addedIndex += 1;
|
|
} else if (removedToken !== undefined) {
|
|
removed.push({ text: removedToken, changed: true });
|
|
removedIndex += 1;
|
|
}
|
|
}
|
|
|
|
return { added: mergeInlineParts(added), removed: mergeInlineParts(removed) };
|
|
}
|
|
|
|
function segmentWords(text: string): string[] {
|
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
return Array.from(segmenter.segment(text), (segment) => segment.segment);
|
|
}
|
|
|
|
function lcsLengths(left: string[], right: string[]): number[][] {
|
|
const rows = Array.from({ length: left.length + 1 }, () => Array.from({ length: right.length + 1 }, () => 0));
|
|
for (let leftIndex = left.length - 1; leftIndex >= 0; leftIndex -= 1) {
|
|
for (let rightIndex = right.length - 1; rightIndex >= 0; rightIndex -= 1) {
|
|
const row = rows[leftIndex];
|
|
if (!row) continue;
|
|
row[rightIndex] =
|
|
left[leftIndex] === right[rightIndex]
|
|
? (rows[leftIndex + 1]?.[rightIndex + 1] ?? 0) + 1
|
|
: Math.max(rows[leftIndex + 1]?.[rightIndex] ?? 0, rows[leftIndex]?.[rightIndex + 1] ?? 0);
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function mergeInlineParts(parts: InlineDiffPart[]): InlineDiffPart[] {
|
|
const merged: InlineDiffPart[] = [];
|
|
for (const part of parts) {
|
|
const previous = merged.at(-1);
|
|
if (previous?.changed === part.changed) {
|
|
previous.text += part.text;
|
|
} else {
|
|
merged.push({ ...part });
|
|
}
|
|
}
|
|
return merged;
|
|
}
|