Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 135 additions & 74 deletions Frontend/src/scripts/features/repo/diffView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ function buildUntrackedTextPatch(path: string, text: string): string[] {
/** Highlights a row in the left list for the current tab. */
export function highlightRow(index: number) {
const rows = qsa<HTMLElement>((prefs.tab === 'history' ? '.row.commit' : '.row'), listEl || (undefined as any));
rows.forEach((el, i) => el.classList.toggle('active', i === index));
rows.forEach((el, i) => {
el.classList.toggle('active', i === index);
});
}

/** Loads and renders the selected file diff with selection state restored. */
Expand Down Expand Up @@ -293,15 +295,19 @@ export function clearDiffSelection() {
if (state.diffSelectedFiles && state.diffSelectedFiles.size > 0) {
state.diffSelectedFiles.clear();
const rows = listEl.querySelectorAll<HTMLElement>('li.row.diffsel');
rows.forEach((r) => r.classList.remove('diffsel'));
rows.forEach((r) => {
r.classList.remove('diffsel');
});
}
}

/** Removes active styling from all rows in the file list. */
export function clearActiveRows() {
if (!listEl) return;
const rows = listEl.querySelectorAll<HTMLElement>('li.row.active');
rows.forEach((r) => r.classList.remove('active'));
rows.forEach((r) => {
r.classList.remove('active');
});
}

/** Loads and renders conflict details and resolution actions. */
Expand Down Expand Up @@ -331,7 +337,7 @@ async function renderConflictView(file: FileStatus) {
function renderConflictMarkup(details: ConflictDetails) {
const binary = !!details.binary;
const header = `<div class="conflict-header"><div class="conflict-title">Merge conflict</div>${renderConflictActions(binary)}</div>`;
const body = binary ? renderBinaryConflictBody(details) : renderTextConflictBody(details);
const body = binary ? renderBinaryConflictBody() : renderTextConflictBody(details);
const pathAttr = escapeHtml(details.path || '');
return `<div class="conflict-view" data-conflict-path="${pathAttr}" data-conflict-binary="${binary ? '1' : '0'}">${header}${body}</div>`;
}
Expand All @@ -347,7 +353,7 @@ function renderConflictActions(binary: boolean) {
}

/** Renders a compact binary-conflict explanation panel. */
function renderBinaryConflictBody(details: ConflictDetails) {
function renderBinaryConflictBody() {
const note = 'This file is binary. Choose which version to keep.';
return `<div class="conflict-body"><div class="conflict-note">${escapeHtml(note)}</div></div>`;
}
Expand Down Expand Up @@ -438,10 +444,10 @@ export function toggleFilePick(path: string, on: boolean) {
(state as any).selectedHunksByFile[state.currentFile] = state.selectedHunks.slice();
const rec: Record<number, number[]> = {};
hunkNodes.forEach((refs, idx) => {
if (refs.hunkCheckbox) {
refs.hunkCheckbox.checked = true;
refs.hunkCheckbox.indeterminate = false;
}
refs.hunkCheckboxes.forEach((box) => {
box.checked = true;
box.indeterminate = false;
});
const picked: number[] = [];
Object.entries(refs.lineCheckboxes).forEach(([key, box]) => {
const lineIdx = Number(key);
Expand All @@ -457,10 +463,10 @@ export function toggleFilePick(path: string, on: boolean) {
delete (state as any).selectedHunksByFile[state.currentFile];
delete (state as any).selectedLinesByFile[state.currentFile];
hunkNodes.forEach((refs) => {
if (refs.hunkCheckbox) {
refs.hunkCheckbox.checked = false;
refs.hunkCheckbox.indeterminate = false;
}
refs.hunkCheckboxes.forEach((box) => {
box.checked = false;
box.indeterminate = false;
});
Object.values(refs.lineCheckboxes).forEach((box) => { box.checked = false; });
});
}
Expand All @@ -478,27 +484,29 @@ export function updateHunkCheckboxes() {
: {};
nodes.forEach((refs, idx) => {
const isSelected = state.selectedHunks.includes(idx);
if (refs.hunkCheckbox) {
refs.hunkCheckbox.checked = isSelected;
refs.hunkCheckbox.indeterminate = false;
}
refs.hunkEl.classList.toggle('picked', isSelected);
refs.hunkCheckboxes.forEach((box) => {
box.checked = isSelected;
box.indeterminate = false;
});
refs.hunkEls.forEach((el) => {
el.classList.toggle('picked', isSelected);
});
if (state.currentFile) {
const lines = rec[idx] || [];
Object.entries(refs.lineCheckboxes).forEach(([key, box]) => {
const lineIdx = Number(key);
const checked = Array.isArray(lines) && lines.includes(lineIdx);
box.checked = checked;
});
if (refs.hunkCheckbox) {
const total = state.currentDiffMeta?.changeCounts[idx] ?? Object.keys(refs.lineCheckboxes).length;
const chosen = lines.length;
refs.hunkCheckbox.checked = total > 0 && chosen === total;
refs.hunkCheckbox.indeterminate = chosen > 0 && chosen < total;
}
const total = state.currentDiffMeta?.changeCounts[idx] ?? Object.keys(refs.lineCheckboxes).length;
const chosen = lines.length;
refs.hunkCheckboxes.forEach((box) => {
box.checked = total > 0 && chosen === total;
box.indeterminate = chosen > 0 && chosen < total;
});
} else {
Object.values(refs.lineCheckboxes).forEach((box) => { box.checked = false; });
if (refs.hunkCheckbox) refs.hunkCheckbox.indeterminate = false;
refs.hunkCheckboxes.forEach((box) => { box.indeterminate = false; });
}
});
}
Expand Down Expand Up @@ -550,11 +558,13 @@ function handleHunkToggle(input: HTMLInputElement) {
}
(state as any).selectedLinesByFile[state.currentFile] = rec;
const refs = state.currentDiffHunkNodes.get(idx);
refs?.hunkEl?.classList.toggle('picked', input.checked);
if (refs?.hunkCheckbox) {
refs.hunkCheckbox.indeterminate = false;
refs.hunkCheckbox.checked = input.checked;
}
refs?.hunkEls.forEach((el) => {
el.classList.toggle('picked', input.checked);
});
refs?.hunkCheckboxes.forEach((box) => {
box.indeterminate = false;
box.checked = input.checked;
});
if (state.currentFile) {
(state as any).selectedHunksByFile[state.currentFile] = state.selectedHunks.slice();
}
Expand Down Expand Up @@ -586,17 +596,20 @@ function handleLineToggle(input: HTMLInputElement) {
(state as any).selectedLinesByFile[state.currentFile] = rec;
const refs = state.currentDiffHunkNodes.get(hunk);
const total = state.currentDiffMeta?.changeCounts[hunk] ?? Object.keys(refs?.lineCheckboxes || {}).length;
const hunkBox = refs?.hunkCheckbox;
if (hunkBox) {
hunkBox.checked = total > 0 && next.length === total;
hunkBox.indeterminate = next.length > 0 && next.length < total;
if (hunkBox.checked) {
if (!state.selectedHunks.includes(hunk)) state.selectedHunks.push(hunk);
} else {
state.selectedHunks = state.selectedHunks.filter((i) => i !== hunk);
}
refs?.hunkEl?.classList.toggle('picked', hunkBox.checked);
const hunkChecked = total > 0 && next.length === total;
const hunkIndeterminate = next.length > 0 && next.length < total;
refs?.hunkCheckboxes.forEach((box) => {
box.checked = hunkChecked;
box.indeterminate = hunkIndeterminate;
});
if (hunkChecked) {
if (!state.selectedHunks.includes(hunk)) state.selectedHunks.push(hunk);
} else {
state.selectedHunks = state.selectedHunks.filter((i) => i !== hunk);
}
refs?.hunkEls.forEach((el) => {
el.classList.toggle('picked', hunkChecked);
});
if (state.currentFile) {
(state as any).selectedHunksByFile[state.currentFile] = state.selectedHunks.slice();
}
Expand Down Expand Up @@ -717,40 +730,22 @@ function buildDiffFragment(lines: string[]): DocumentFragment {
const e = meta.starts[h + 1];
const hunkLines = meta.rest.slice(s, e);
const offset = meta.offset + s;
const hunkEl = document.createElement('div');
hunkEl.className = 'hunk';
hunkEl.dataset.hunkIndex = String(h);

const header = document.createElement('div');
header.className = 'hline';
const gutter = document.createElement('div');
gutter.className = 'gutter';
const label = document.createElement('label');
label.className = 'pick-toggle';
const hunkCheckbox = document.createElement('input');
hunkCheckbox.type = 'checkbox';
hunkCheckbox.className = 'pick-hunk';
hunkCheckbox.dataset.hunk = String(h);
label.appendChild(hunkCheckbox);
const srHunk = document.createElement('span');
srHunk.className = 'sr-only';
srHunk.textContent = 'Include hunk';
label.appendChild(srHunk);
gutter.appendChild(label);
header.appendChild(gutter);
const codeHeader = document.createElement('div');
codeHeader.className = 'code';
header.appendChild(codeHeader);
hunkEl.appendChild(header);

const lineCheckboxes: Record<number, HTMLInputElement> = {};
hunkLines.forEach((ln, i) => {
const lineRows = hunkLines.map((ln, i) => {
const first = (typeof ln === 'string' ? ln[0] : ' ') || ' ';
const lineRow = document.createElement('div');
lineRow.className = `hline${first === '+' ? ' add' : first === '-' ? ' del' : ''}`;
const lineGutter = document.createElement('div');
lineGutter.className = 'gutter';
if (first === '+' || first === '-') {
const leftNumber = document.createElement('span');
leftNumber.className = 'line-number line-number-left';
const rightNumber = document.createElement('span');
rightNumber.className = 'line-number line-number-right';
const numberText = String(offset + i + 1);
if (first === '-') leftNumber.textContent = numberText;
else rightNumber.textContent = numberText;
lineGutter.appendChild(leftNumber);
const lineLabel = document.createElement('label');
lineLabel.className = 'pick-toggle';
const lineCheckbox = document.createElement('input');
Expand All @@ -764,18 +759,65 @@ function buildDiffFragment(lines: string[]): DocumentFragment {
srLine.textContent = 'Include line';
lineLabel.appendChild(srLine);
lineGutter.appendChild(lineLabel);
lineGutter.appendChild(rightNumber);
lineCheckboxes[i] = lineCheckbox;
}
lineGutter.appendChild(document.createTextNode(String(offset + i + 1)));
if (first !== '+' && first !== '-') {
const lineNumber = document.createElement('span');
lineNumber.className = 'line-number line-number-right';
lineNumber.textContent = String(offset + i + 1);
lineGutter.appendChild(lineNumber);
}
const code = document.createElement('div');
code.className = 'code';
code.innerHTML = escapeHtml(String(ln || ''));
lineRow.appendChild(lineGutter);
lineRow.appendChild(code);
hunkEl.appendChild(lineRow);
return lineRow;
});
nodes.set(h, { hunkEl, hunkCheckbox, lineCheckboxes });
fragment.appendChild(hunkEl);
const hunkEls: HTMLElement[] = [];
const hunkCheckboxes: HTMLInputElement[] = [];
let currentSegmentRows: HTMLElement[] = [];
const flushSegment = () => {
if (currentSegmentRows.length === 0) return;
const hunkEl = document.createElement('div');
hunkEl.className = 'hunk';
hunkEl.dataset.hunkIndex = String(h);
const selectionBody = document.createElement('div');
selectionBody.className = 'hunk-selection-body';
const hunkCheckbox = document.createElement('input');
hunkCheckbox.type = 'checkbox';
hunkCheckbox.className = 'pick-hunk';
hunkCheckbox.dataset.hunk = String(h);
const label = document.createElement('label');
label.className = 'pick-toggle';
label.appendChild(hunkCheckbox);
const srHunk = document.createElement('span');
srHunk.className = 'sr-only';
srHunk.textContent = 'Include hunk';
label.appendChild(srHunk);
selectionBody.appendChild(label);
currentSegmentRows.forEach((row) => {
selectionBody.appendChild(row);
});
hunkEl.appendChild(selectionBody);
fragment.appendChild(hunkEl);
hunkEls.push(hunkEl);
hunkCheckboxes.push(hunkCheckbox);
currentSegmentRows = [];
};
hunkLines.forEach((ln, i) => {
const first = (typeof ln === 'string' ? ln[0] : ' ') || ' ';
const row = lineRows[i];
if (first === '+' || first === '-') {
currentSegmentRows.push(row);
return;
}
flushSegment();
fragment.appendChild(row);
});
flushSegment();
nodes.set(h, { hunkEls, hunkCheckboxes, lineCheckboxes });
}
state.currentDiffHunkNodes = nodes;
return fragment;
Expand All @@ -801,11 +843,30 @@ export function renderHunksWithSelection(lines: string[]) {
const body = hunkLines.map((ln, i) => {
const first = (typeof ln === 'string' ? ln[0] : ' ') || ' ';
const isChange = first === '+' || first === '-';
const numberText = `${offset + i + 1}`;
const leftNumber = isChange && first === '-' ? `<span class="line-number line-number-left">${numberText}</span>` : isChange ? `<span class="line-number line-number-left"></span>` : '';
const rightNumber = isChange && first === '+' ? `<span class="line-number line-number-right">${numberText}</span>` : isChange ? `<span class="line-number line-number-right"></span>` : `<span class="line-number line-number-right">${numberText}</span>`;
const lineCheckbox = isChange ? `<label class="pick-toggle"><input type="checkbox" class="pick-line" data-hunk="${h}" data-line="${i}" /><span class="sr-only">Include line</span></label>` : '';
const t = first === '+' ? 'add' : first === '-' ? 'del' : '';
return `<div class="hline ${t}"><div class="gutter">${lineCheckbox}${offset + i + 1}</div><div class="code">${escapeHtml(String(ln))}</div></div>`;
}).join('');
html += `<div class="hunk" data-hunk-index="${h}"><div class="hline"><div class="gutter"><label class="pick-toggle"><input type="checkbox" class="pick-hunk" data-hunk="${h}" /><span class="sr-only">Include hunk</span></label></div><div class="code"></div></div>${body}</div>`;
return `<div class="hline ${t}"><div class="gutter">${leftNumber}${lineCheckbox}${rightNumber}</div><div class="code">${escapeHtml(String(ln))}</div></div>`;
});
let currentSegmentRows: string[] = [];
const flushSegment = () => {
if (currentSegmentRows.length === 0) return;
html += `<div class="hunk" data-hunk-index="${h}"><div class="hunk-selection-body"><label class="pick-toggle"><input type="checkbox" class="pick-hunk" data-hunk="${h}" /><span class="sr-only">Include hunk</span></label>${currentSegmentRows.join('')}</div></div>`;
currentSegmentRows = [];
};
hunkLines.forEach((ln, i) => {
const first = (typeof ln === 'string' ? ln[0] : ' ') || ' ';
const row = body[i];
if (first === '+' || first === '-') {
currentSegmentRows.push(row);
return;
}
flushSegment();
html += row;
});
flushSegment();
}
return html;
}
Expand Down
4 changes: 2 additions & 2 deletions Frontend/src/scripts/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export type DiffMeta = {

/** References to DOM elements for a hunk. */
export type HunkNodeRefs = {
hunkEl: HTMLElement;
hunkCheckbox: HTMLInputElement | null;
hunkEls: HTMLElement[];
hunkCheckboxes: HTMLInputElement[];
lineCheckboxes: Record<number, HTMLInputElement>;
};

Expand Down
Loading
Loading