Archived
Merge branch 'main' into fix/persist-subsession-links
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Highlight within-line changes in the Git diff viewer.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { LitElement, css, html, type TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { parseUnifiedDiff, type UnifiedDiffLine, type UnifiedDiffTextSpan } from "../diff/unifiedDiff";
|
||||||
|
|
||||||
|
@customElement("unified-diff-viewer")
|
||||||
|
export class UnifiedDiffViewer extends LitElement {
|
||||||
|
@property() diff = "";
|
||||||
|
|
||||||
|
override render(): TemplateResult {
|
||||||
|
const lines = parseUnifiedDiff(this.diff);
|
||||||
|
if (lines.length === 0) return html`<p class="empty">No diff.</p>`;
|
||||||
|
return html`
|
||||||
|
<div class="scroller">
|
||||||
|
<div class="diff-grid" role="table" aria-label="Unified diff">
|
||||||
|
${lines.map((line) => this.renderLine(line))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLine(line: UnifiedDiffLine): TemplateResult {
|
||||||
|
const kindClass = line.kind;
|
||||||
|
return html`
|
||||||
|
<div class="line" role="row">
|
||||||
|
<span class=${`cell line-number old ${kindClass}`} role="cell">${formatLineNumber(line.oldLineNumber)}</span>
|
||||||
|
<span class=${`cell line-number new ${kindClass}`} role="cell">${formatLineNumber(line.newLineNumber)}</span>
|
||||||
|
<span class=${`cell prefix ${kindClass}`} role="cell">${line.prefix}</span>
|
||||||
|
<span class=${`cell content ${kindClass}`} role="cell">${renderSpans(line.spans)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static override styles = css`
|
||||||
|
:host { display: block; min-height: 0; height: 100%; color: var(--pi-text); background: var(--pi-bg); }
|
||||||
|
.empty { box-sizing: border-box; margin: 0; padding: 10px; color: var(--pi-muted); }
|
||||||
|
.scroller { height: 100%; min-height: 0; overflow: auto; background: var(--pi-bg); }
|
||||||
|
.diff-grid { display: grid; grid-template-columns: max-content max-content 2ch max-content; width: max-content; min-width: 100%; padding: 6px 0; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
|
||||||
|
.line { display: contents; }
|
||||||
|
.cell { min-height: 1.45em; white-space: pre; }
|
||||||
|
.line-number { min-width: 4ch; padding: 0 8px; border-right: 1px solid var(--pi-border-muted); color: var(--pi-dim); text-align: right; user-select: none; }
|
||||||
|
.prefix { padding: 0 4px; color: var(--pi-dim); text-align: center; user-select: none; }
|
||||||
|
.content { padding: 0 12px 0 4px; }
|
||||||
|
.meta { color: var(--pi-dim); }
|
||||||
|
.hunk { background: color-mix(in srgb, var(--pi-accent) 9%, transparent); color: var(--pi-accent); }
|
||||||
|
.add { background: color-mix(in srgb, var(--pi-success) 12%, transparent); }
|
||||||
|
.remove { background: color-mix(in srgb, var(--pi-danger) 12%, transparent); }
|
||||||
|
.marker { color: var(--pi-dim); }
|
||||||
|
.content.add .inline-change { border-radius: 2px; background: color-mix(in srgb, var(--pi-success) 36%, transparent); color: var(--pi-text); }
|
||||||
|
.content.remove .inline-change { border-radius: 2px; background: color-mix(in srgb, var(--pi-danger) 36%, transparent); color: var(--pi-text); }
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSpans(spans: UnifiedDiffTextSpan[]): TemplateResult[] {
|
||||||
|
return spans.map((span) => html`<span class=${span.changed ? "inline-change" : ""}>${span.text}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLineNumber(lineNumber: number | undefined): string {
|
||||||
|
return lineNumber === undefined ? "" : String(lineNumber);
|
||||||
|
}
|
||||||
@@ -200,7 +200,7 @@ export const workspacePanelStyles = css`
|
|||||||
.diff-section:last-child { border-bottom: 0; }
|
.diff-section:last-child { border-bottom: 0; }
|
||||||
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); }
|
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); }
|
||||||
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
code-viewer { flex: 1 1 auto; min-height: 0; }
|
code-viewer, unified-diff-viewer { flex: 1 1 auto; min-height: 0; }
|
||||||
.image-preview { flex: 1 1 auto; min-height: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 16px; }
|
.image-preview { flex: 1 1 auto; min-height: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 16px; }
|
||||||
.image-preview img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; border: 1px solid var(--pi-border-muted); border-radius: 8px; background-color: var(--pi-surface); background-image: linear-gradient(45deg, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 25%, transparent 25%), linear-gradient(-45deg, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 75%), linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 75%); background-position: 0 0, 0 8px, 8px -8px, -8px 0; background-size: 16px 16px; box-shadow: 0 8px 24px var(--pi-shadow-soft); }
|
.image-preview img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; border: 1px solid var(--pi-border-muted); border-radius: 8px; background-color: var(--pi-surface); background-image: linear-gradient(45deg, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 25%, transparent 25%), linear-gradient(-45deg, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 75%), linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--pi-border-muted) 45%, transparent) 75%); background-position: 0 0, 0 8px, 8px -8px, -8px 0; background-size: 16px 16px; box-shadow: 0 8px 24px var(--pi-shadow-soft); }
|
||||||
pre { margin: 0; padding: 10px; overflow: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
|
pre { margin: 0; padding: 10px; overflow: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseUnifiedDiff, type UnifiedDiffLine, type UnifiedDiffLineKind } from "./unifiedDiff";
|
||||||
|
|
||||||
|
describe("parseUnifiedDiff", () => {
|
||||||
|
it("computes inline spans for paired removed and added lines", () => {
|
||||||
|
const diff = [
|
||||||
|
"diff --git a/src/app.ts b/src/app.ts",
|
||||||
|
"index 1111111..2222222 100644",
|
||||||
|
"--- a/src/app.ts",
|
||||||
|
"+++ b/src/app.ts",
|
||||||
|
"@@ -10,2 +10,2 @@ export function demo() {",
|
||||||
|
"- const name = \"fooBar\";",
|
||||||
|
"+ const name = \"fooBaz\";",
|
||||||
|
" return name;",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const lines = parseUnifiedDiff(diff);
|
||||||
|
const removed = firstLineOfKind(lines, "remove");
|
||||||
|
const added = firstLineOfKind(lines, "add");
|
||||||
|
const context = firstLineOfKind(lines, "context");
|
||||||
|
|
||||||
|
expect(removed.oldLineNumber).toBe(10);
|
||||||
|
expect(removed.newLineNumber).toBeUndefined();
|
||||||
|
expect(changedText(removed)).toEqual(["r"]);
|
||||||
|
expect(added.oldLineNumber).toBeUndefined();
|
||||||
|
expect(added.newLineNumber).toBe(10);
|
||||||
|
expect(changedText(added)).toEqual(["z"]);
|
||||||
|
expect(context.oldLineNumber).toBe(11);
|
||||||
|
expect(context.newLineNumber).toBe(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps file headers as metadata before a hunk starts", () => {
|
||||||
|
const diff = [
|
||||||
|
"diff --git a/README.md b/README.md",
|
||||||
|
"index 1111111..2222222 100644",
|
||||||
|
"--- a/README.md",
|
||||||
|
"+++ b/README.md",
|
||||||
|
"@@ -1 +1 @@",
|
||||||
|
"-old",
|
||||||
|
"+new",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
expect(parseUnifiedDiff(diff).slice(0, 5).map((line) => line.kind)).toEqual(["meta", "meta", "meta", "meta", "hunk"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses changed content that starts with file header markers inside hunks", () => {
|
||||||
|
const diff = [
|
||||||
|
"diff --git a/README.md b/README.md",
|
||||||
|
"--- a/README.md",
|
||||||
|
"+++ b/README.md",
|
||||||
|
"@@ -1 +1 @@",
|
||||||
|
"---- removed heading",
|
||||||
|
"++++ added heading",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const removed = firstLineOfKind(parseUnifiedDiff(diff), "remove");
|
||||||
|
const added = firstLineOfKind(parseUnifiedDiff(diff), "add");
|
||||||
|
|
||||||
|
expect(removed.text).toBe("--- removed heading");
|
||||||
|
expect(added.text).toBe("+++ added heading");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pairs a single removed line with the closest added line in uneven blocks", () => {
|
||||||
|
const diff = [
|
||||||
|
"diff --git a/src/app.ts b/src/app.ts",
|
||||||
|
"--- a/src/app.ts",
|
||||||
|
"+++ b/src/app.ts",
|
||||||
|
"@@ -1 +1,2 @@",
|
||||||
|
"-const label = \"old\";",
|
||||||
|
"+const label = \"new\";",
|
||||||
|
"+const extra = true;",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const addedLines = linesOfKind(parseUnifiedDiff(diff), "add");
|
||||||
|
const firstAdded = lineAt(addedLines, 0);
|
||||||
|
const secondAdded = lineAt(addedLines, 1);
|
||||||
|
|
||||||
|
expect(changedText(firstAdded)).toEqual(["new"]);
|
||||||
|
expect(secondAdded.spans.every((span) => !span.changed)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves pure additions without inline change spans", () => {
|
||||||
|
const diff = [
|
||||||
|
"diff --git a/new.txt b/new.txt",
|
||||||
|
"new file mode 100644",
|
||||||
|
"--- /dev/null",
|
||||||
|
"+++ b/new.txt",
|
||||||
|
"@@ -0,0 +1 @@",
|
||||||
|
"+brand new",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const added = firstLineOfKind(parseUnifiedDiff(diff), "add");
|
||||||
|
|
||||||
|
expect(added.newLineNumber).toBe(1);
|
||||||
|
expect(added.spans).toEqual([{ text: "brand new", changed: false }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function firstLineOfKind(lines: UnifiedDiffLine[], kind: UnifiedDiffLineKind): UnifiedDiffLine {
|
||||||
|
const found = lines.find((line) => line.kind === kind);
|
||||||
|
if (found === undefined) throw new Error(`Missing ${kind} line`);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesOfKind(lines: UnifiedDiffLine[], kind: UnifiedDiffLineKind): UnifiedDiffLine[] {
|
||||||
|
return lines.filter((line) => line.kind === kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineAt(lines: UnifiedDiffLine[], index: number): UnifiedDiffLine {
|
||||||
|
const line = lines[index];
|
||||||
|
if (line === undefined) throw new Error(`Missing line at ${String(index)}`);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changedText(line: UnifiedDiffLine): string[] {
|
||||||
|
return line.spans.filter((span) => span.changed).map((span) => span.text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import { diffChars } from "diff";
|
||||||
|
|
||||||
|
export type UnifiedDiffLineKind = "meta" | "hunk" | "context" | "add" | "remove" | "marker";
|
||||||
|
|
||||||
|
export interface UnifiedDiffTextSpan {
|
||||||
|
text: string;
|
||||||
|
changed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnifiedDiffLine {
|
||||||
|
kind: UnifiedDiffLineKind;
|
||||||
|
prefix: string;
|
||||||
|
text: string;
|
||||||
|
spans: UnifiedDiffTextSpan[];
|
||||||
|
oldLineNumber?: number;
|
||||||
|
newLineNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InlineDiffResult {
|
||||||
|
removed: UnifiedDiffTextSpan[];
|
||||||
|
added: UnifiedDiffTextSpan[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffLinePair {
|
||||||
|
removed: UnifiedDiffLine;
|
||||||
|
added: UnifiedDiffLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
||||||
|
const maxInlineLineLength = 5_000;
|
||||||
|
const maxInlineBlockLines = 20;
|
||||||
|
const minInlineSimilarity = 0.20;
|
||||||
|
const minPairSimilarity = 0.25;
|
||||||
|
|
||||||
|
export function parseUnifiedDiff(diff: string): UnifiedDiffLine[] {
|
||||||
|
const parsedLines = parseUnifiedDiffLines(diff);
|
||||||
|
applyInlineDiffs(parsedLines);
|
||||||
|
return parsedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUnifiedDiffLines(diff: string): UnifiedDiffLine[] {
|
||||||
|
const lines = splitDiffLines(diff);
|
||||||
|
const parsedLines: UnifiedDiffLine[] = [];
|
||||||
|
let oldLineNumber: number | undefined;
|
||||||
|
let newLineNumber: number | undefined;
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const hunkMatch = hunkHeaderPattern.exec(rawLine);
|
||||||
|
if (hunkMatch !== null) {
|
||||||
|
oldLineNumber = Number(hunkMatch[1]);
|
||||||
|
newLineNumber = Number(hunkMatch[2]);
|
||||||
|
parsedLines.push(line("hunk", "", rawLine));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldLineNumber !== undefined && newLineNumber !== undefined) {
|
||||||
|
if (rawLine.startsWith("+")) {
|
||||||
|
parsedLines.push(line("add", "+", rawLine.slice(1), { newLineNumber }));
|
||||||
|
newLineNumber++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rawLine.startsWith("-")) {
|
||||||
|
parsedLines.push(line("remove", "-", rawLine.slice(1), { oldLineNumber }));
|
||||||
|
oldLineNumber++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rawLine.startsWith(" ")) {
|
||||||
|
parsedLines.push(line("context", " ", rawLine.slice(1), { oldLineNumber, newLineNumber }));
|
||||||
|
oldLineNumber++;
|
||||||
|
newLineNumber++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rawLine.startsWith("\\")) {
|
||||||
|
parsedLines.push(line("marker", "", rawLine));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLineNumber = undefined;
|
||||||
|
newLineNumber = undefined;
|
||||||
|
parsedLines.push(line("meta", "", rawLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitDiffLines(diff: string): string[] {
|
||||||
|
if (diff === "") return [];
|
||||||
|
const lines = diff.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
||||||
|
if (lines.at(-1) === "") lines.pop();
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function line(kind: UnifiedDiffLineKind, prefix: string, text: string, numbers: { oldLineNumber?: number; newLineNumber?: number } = {}): UnifiedDiffLine {
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
prefix,
|
||||||
|
text,
|
||||||
|
spans: text === "" ? [] : [{ text, changed: false }],
|
||||||
|
...numbers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInlineDiffs(lines: UnifiedDiffLine[]): void {
|
||||||
|
let index = 0;
|
||||||
|
while (index < lines.length) {
|
||||||
|
const current = lines[index];
|
||||||
|
if (current?.kind !== "remove") {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedStart = index;
|
||||||
|
while (lines[index]?.kind === "remove") index++;
|
||||||
|
const addedStart = index;
|
||||||
|
while (lines[index]?.kind === "add") index++;
|
||||||
|
|
||||||
|
if (addedStart === index) continue;
|
||||||
|
const removedLines = lines.slice(removedStart, addedStart);
|
||||||
|
const addedLines = lines.slice(addedStart, index);
|
||||||
|
applyInlineDiffBlock(removedLines, addedLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInlineDiffBlock(removedLines: UnifiedDiffLine[], addedLines: UnifiedDiffLine[]): void {
|
||||||
|
if (removedLines.length + addedLines.length > maxInlineBlockLines) return;
|
||||||
|
for (const pair of pairChangedLines(removedLines, addedLines)) {
|
||||||
|
const inlineDiff = computeInlineDiff(pair.removed.text, pair.added.text);
|
||||||
|
if (inlineDiff === undefined) continue;
|
||||||
|
pair.removed.spans = inlineDiff.removed;
|
||||||
|
pair.added.spans = inlineDiff.added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pairChangedLines(removedLines: UnifiedDiffLine[], addedLines: UnifiedDiffLine[]): DiffLinePair[] {
|
||||||
|
if (removedLines.length === addedLines.length) return removedLines.map((removed, index) => ({ removed, added: addedLines[index] })).filter(isCompletePair);
|
||||||
|
if (removedLines.length === 1) return bestPairsForSingleRemovedLine(removedLines[0], addedLines);
|
||||||
|
if (addedLines.length === 1) return bestPairsForSingleAddedLine(removedLines, addedLines[0]);
|
||||||
|
|
||||||
|
const pairs: DiffLinePair[] = [];
|
||||||
|
const pairCount = Math.min(removedLines.length, addedLines.length);
|
||||||
|
for (let index = 0; index < pairCount; index++) {
|
||||||
|
const removed = removedLines[index];
|
||||||
|
const added = addedLines[index];
|
||||||
|
if (removed === undefined || added === undefined) continue;
|
||||||
|
if (lineSimilarity(removed.text, added.text) >= minPairSimilarity) pairs.push({ removed, added });
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompletePair(pair: { removed: UnifiedDiffLine; added: UnifiedDiffLine | undefined }): pair is DiffLinePair {
|
||||||
|
return pair.added !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestPairsForSingleRemovedLine(removed: UnifiedDiffLine | undefined, addedLines: UnifiedDiffLine[]): DiffLinePair[] {
|
||||||
|
if (removed === undefined) return [];
|
||||||
|
const added = bestMatchingLine(removed.text, addedLines);
|
||||||
|
return added === undefined ? [] : [{ removed, added }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestPairsForSingleAddedLine(removedLines: UnifiedDiffLine[], added: UnifiedDiffLine | undefined): DiffLinePair[] {
|
||||||
|
if (added === undefined) return [];
|
||||||
|
const removed = bestMatchingLine(added.text, removedLines);
|
||||||
|
return removed === undefined ? [] : [{ removed, added }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestMatchingLine(text: string, candidates: UnifiedDiffLine[]): UnifiedDiffLine | undefined {
|
||||||
|
let bestCandidate: UnifiedDiffLine | undefined;
|
||||||
|
let bestScore = minPairSimilarity;
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const score = lineSimilarity(text, candidate.text);
|
||||||
|
if (score <= bestScore) continue;
|
||||||
|
bestCandidate = candidate;
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
return bestCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeInlineDiff(oldText: string, newText: string): InlineDiffResult | undefined {
|
||||||
|
if (oldText === newText) return undefined;
|
||||||
|
if (oldText.length > maxInlineLineLength || newText.length > maxInlineLineLength) return undefined;
|
||||||
|
|
||||||
|
const changes = diffChars(oldText, newText);
|
||||||
|
const similarity = similarityFromChanges(changes, oldText, newText);
|
||||||
|
if (Math.max(oldText.length, newText.length) >= 20 && similarity < minInlineSimilarity) return undefined;
|
||||||
|
|
||||||
|
const removed: UnifiedDiffTextSpan[] = [];
|
||||||
|
const added: UnifiedDiffTextSpan[] = [];
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.value === "") continue;
|
||||||
|
if (change.added) added.push({ text: change.value, changed: true });
|
||||||
|
else if (change.removed) removed.push({ text: change.value, changed: true });
|
||||||
|
else {
|
||||||
|
removed.push({ text: change.value, changed: false });
|
||||||
|
added.push({ text: change.value, changed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed.some((span) => span.changed) && !added.some((span) => span.changed)) return undefined;
|
||||||
|
return { removed: mergeAdjacentSpans(removed), added: mergeAdjacentSpans(added) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineSimilarity(oldText: string, newText: string): number {
|
||||||
|
if (oldText === newText) return 1;
|
||||||
|
if (oldText.length > maxInlineLineLength || newText.length > maxInlineLineLength) return 0;
|
||||||
|
return similarityFromChanges(diffChars(oldText, newText), oldText, newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function similarityFromChanges(changes: ReturnType<typeof diffChars>, oldText: string, newText: string): number {
|
||||||
|
const maxLength = Math.max(oldText.length, newText.length);
|
||||||
|
if (maxLength === 0) return 1;
|
||||||
|
const unchangedLength = changes.reduce((total, change) => change.added || change.removed ? total : total + change.value.length, 0);
|
||||||
|
return unchangedLength / maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAdjacentSpans(spans: UnifiedDiffTextSpan[]): UnifiedDiffTextSpan[] {
|
||||||
|
const merged: UnifiedDiffTextSpan[] = [];
|
||||||
|
for (const span of spans) {
|
||||||
|
const previous = merged[merged.length - 1];
|
||||||
|
if (previous?.changed === span.changed) previous.text += span.text;
|
||||||
|
else merged.push({ ...span });
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
@@ -146,11 +146,11 @@ function renderDiffViewer(context: WorkspacePanelContext): TemplateResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDiffSection(diff: GitDiffResponse): TemplateResult {
|
function renderDiffSection(diff: GitDiffResponse): TemplateResult {
|
||||||
loadCodeViewer();
|
loadUnifiedDiffViewer();
|
||||||
return html`
|
return html`
|
||||||
<section class="diff-section">
|
<section class="diff-section">
|
||||||
<div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
|
<div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
|
||||||
<code-viewer .content=${diff.diff} .language=${"diff"}></code-viewer>
|
<unified-diff-viewer .diff=${diff.diff}></unified-diff-viewer>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -159,6 +159,10 @@ function loadCodeViewer(): void {
|
|||||||
void import("../../components/CodeViewer");
|
void import("../../components/CodeViewer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadUnifiedDiffViewer(): void {
|
||||||
|
void import("../../components/UnifiedDiffViewer");
|
||||||
|
}
|
||||||
|
|
||||||
function loadTerminalPanel(): void {
|
function loadTerminalPanel(): void {
|
||||||
void import("../../components/TerminalPanel");
|
void import("../../components/TerminalPanel");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user