mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-15 21:45:35 +08:00
Merge 4f65ae332a into 7190519fb3
This commit is contained in:
commit
451d026543
@ -11,7 +11,8 @@
|
||||
</td>
|
||||
{{else}}
|
||||
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
||||
{{- $leftAnchor := Iif $line.LeftIdx (printf "diff-%sL%d" $.FileNameHash $line.LeftIdx) "" -}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span{{if $leftAnchor}} id="{{$leftAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-old">
|
||||
@ -27,7 +28,8 @@
|
||||
<code class="code-inner"></code>
|
||||
{{- end -}}
|
||||
</td>
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
|
||||
{{- $rightAnchor := Iif $line.RightIdx (printf "diff-%sR%d" $.FileNameHash $line.RightIdx) "" -}}
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span{{if $rightAnchor}} id="{{$rightAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-new">
|
||||
@ -65,8 +67,10 @@
|
||||
{{if eq .GetType 4}}
|
||||
<td colspan="2" class="lines-num">{{$line.RenderBlobExcerptButtons $.FileNameHash $diffBlobExcerptData}}</td>
|
||||
{{else}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
|
||||
{{- $leftAnchor := Iif $line.LeftIdx (printf "diff-%sL%d" $.FileNameHash $line.LeftIdx) "" -}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span{{if $leftAnchor}} id="{{$leftAnchor}}"{{end}}></span></td>
|
||||
{{- $rightAnchor := Iif $line.RightIdx (printf "diff-%sR%d" $.FileNameHash $line.RightIdx) "" -}}
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span{{if $rightAnchor}} id="{{$rightAnchor}}"{{end}}></span></td>
|
||||
{{end}}
|
||||
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
|
||||
<td class="lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
{{$match := index $section.Lines $line.Match}}
|
||||
{{- $leftDiff := ""}}{{if $line.LeftIdx}}{{$leftDiff = $section.GetComputedInlineDiffFor $line ctx.Locale}}{{end}}
|
||||
{{- $rightDiff := ""}}{{if $match.RightIdx}}{{$rightDiff = $section.GetComputedInlineDiffFor $match ctx.Locale}}{{end}}
|
||||
<td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span rel="diff-{{$file.NameHash}}L{{$line.LeftIdx}}"></span></td>
|
||||
{{- $leftAnchor := Iif $line.LeftIdx (printf "diff-%sL%d" $file.NameHash $line.LeftIdx) "" -}}
|
||||
<td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span{{if $leftAnchor}} id="{{$leftAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
|
||||
<td class="lines-code lines-code-old del-code">
|
||||
@ -39,7 +40,8 @@
|
||||
<code class="code-inner"></code>
|
||||
{{- end -}}
|
||||
</td>
|
||||
<td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span rel="{{if $match.RightIdx}}diff-{{$file.NameHash}}R{{$match.RightIdx}}{{end}}"></span></td>
|
||||
{{- $matchRightAnchor := Iif $match.RightIdx (printf "diff-%sR%d" $file.NameHash $match.RightIdx) "" -}}
|
||||
<td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span{{if $matchRightAnchor}} id="{{$matchRightAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-new add-code">
|
||||
@ -56,7 +58,8 @@
|
||||
</td>
|
||||
{{else}}
|
||||
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
||||
{{- $leftAnchor := Iif $line.LeftIdx (printf "diff-%sL%d" $file.NameHash $line.LeftIdx) "" -}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span{{if $leftAnchor}} id="{{$leftAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-old">
|
||||
@ -71,7 +74,8 @@
|
||||
<code class="code-inner"></code>
|
||||
{{- end -}}
|
||||
</td>
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
|
||||
{{- $rightAnchor := Iif $line.RightIdx (printf "diff-%sR%d" $file.NameHash $line.RightIdx) "" -}}
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span{{if $rightAnchor}} id="{{$rightAnchor}}"{{end}}></span></td>
|
||||
<td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-new">
|
||||
|
||||
@ -19,8 +19,10 @@
|
||||
<td colspan="2" class="lines-num"></td>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
|
||||
{{- $leftAnchor := Iif $line.LeftIdx (printf "diff-%sL%d" $file.NameHash $line.LeftIdx) "" -}}
|
||||
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span{{if $leftAnchor}} id="{{$leftAnchor}}"{{end}}></span></td>
|
||||
{{- $rightAnchor := Iif $line.RightIdx (printf "diff-%sR%d" $file.NameHash $line.RightIdx) "" -}}
|
||||
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span{{if $rightAnchor}} id="{{$rightAnchor}}"{{end}}></span></td>
|
||||
{{end}}
|
||||
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale -}}
|
||||
<td class="lines-escape">
|
||||
|
||||
@ -986,6 +986,14 @@ td .commit-summary {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff .lines-num[data-line-num] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff .lines-num[data-line-num]:hover {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff tbody tr .lines-type-marker {
|
||||
width: 10px;
|
||||
min-width: 10px;
|
||||
@ -997,6 +1005,32 @@ td .commit-summary {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff tr.active .lines-num,
|
||||
.repository .diff-file-box .code-diff tr.active .lines-escape,
|
||||
.repository .diff-file-box .code-diff tr.active .lines-type-marker,
|
||||
.repository .diff-file-box .code-diff tr.active .lines-code {
|
||||
background: var(--color-highlight-bg);
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff tr.active .lines-num {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff tr.active .lines-num::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff tr.active .lines-num:first-of-type::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--color-highlight-fg);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.repository .diff-file-box .code-diff-split .tag-code .lines-code code.code-inner {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
@ -1,19 +1,60 @@
|
||||
import {svg} from '../svg.ts';
|
||||
|
||||
function parseTransitionValue(value: string): number {
|
||||
let max = 0;
|
||||
for (const current of value.split(',')) {
|
||||
const trimmed = current.trim();
|
||||
if (!trimmed) continue;
|
||||
const isMs = trimmed.endsWith('ms');
|
||||
const numericPortion = Number.parseFloat(trimmed.replace(/ms|s$/u, ''));
|
||||
if (Number.isNaN(numericPortion)) continue;
|
||||
const duration = numericPortion * (isMs ? 1 : 1000);
|
||||
max = Math.max(max, duration);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function waitForTransitionEnd(element: Element): Promise<void> {
|
||||
if (!(element instanceof HTMLElement)) return Promise.resolve();
|
||||
const transitionTarget = element.querySelector<HTMLElement>('.diff-file-body') ?? element;
|
||||
const styles = window.getComputedStyle(transitionTarget);
|
||||
const transitionDuration = parseTransitionValue(styles.transitionDuration);
|
||||
const transitionDelay = parseTransitionValue(styles.transitionDelay);
|
||||
const total = transitionDuration + transitionDelay;
|
||||
if (total === 0) return Promise.resolve();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
function cleanup() {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
transitionTarget.removeEventListener('transitionend', onTransitionEnd);
|
||||
resolve();
|
||||
}
|
||||
function onTransitionEnd(event: TransitionEvent) {
|
||||
if (event.target !== transitionTarget) return;
|
||||
cleanup();
|
||||
}
|
||||
transitionTarget.addEventListener('transitionend', onTransitionEnd);
|
||||
window.setTimeout(cleanup, total + 50);
|
||||
});
|
||||
}
|
||||
|
||||
// Hides the file if newFold is true, and shows it otherwise. The actual hiding is performed using CSS.
|
||||
//
|
||||
// The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
|
||||
// The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
|
||||
//
|
||||
export function setFileFolding(fileContentBox: Element, foldArrow: HTMLElement, newFold: boolean) {
|
||||
export function setFileFolding(fileContentBox: Element, foldArrow: HTMLElement, newFold: boolean): Promise<void> {
|
||||
foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
|
||||
fileContentBox.setAttribute('data-folded', String(newFold));
|
||||
if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
|
||||
fileContentBox.scrollIntoView();
|
||||
}
|
||||
return waitForTransitionEnd(fileContentBox);
|
||||
}
|
||||
|
||||
// Like `setFileFolding`, except that it automatically inverts the current file folding state.
|
||||
export function invertFileFolding(fileContentBox:HTMLElement, foldArrow: HTMLElement) {
|
||||
setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
|
||||
export function invertFileFolding(fileContentBox:HTMLElement, foldArrow: HTMLElement): Promise<void> {
|
||||
return setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
|
||||
}
|
||||
|
||||
@ -3,14 +3,6 @@ import {createTippy} from '../modules/tippy.ts';
|
||||
import {toAbsoluteUrl} from '../utils.ts';
|
||||
import {addDelegatedEventListener} from '../utils/dom.ts';
|
||||
|
||||
function changeHash(hash: string) {
|
||||
if (window.history.pushState) {
|
||||
window.history.pushState(null, '', hash);
|
||||
} else {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
}
|
||||
|
||||
// it selects the code lines defined by range: `L1-L3` (3 lines) or `L2` (singe line)
|
||||
function selectRange(range: string): Element | null {
|
||||
for (const el of document.querySelectorAll('.code-view tr.active')) el.classList.remove('active');
|
||||
@ -65,7 +57,7 @@ function selectRange(range: string): Element | null {
|
||||
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
|
||||
elLineNums[i].closest('tr')!.classList.add('active');
|
||||
}
|
||||
changeHash(`#${range}`);
|
||||
window.history.replaceState(null, '', `#${range}`);
|
||||
updateIssueHref(range);
|
||||
updateViewGitBlameFragment(range);
|
||||
updateCopyPermalinkUrl(range);
|
||||
|
||||
86
web_src/js/features/repo-diff-selection.test.ts
Normal file
86
web_src/js/features/repo-diff-selection.test.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {applyDiffLineSelection} from './repo-diff-selection.ts';
|
||||
|
||||
function createDiffRow(tbody: HTMLTableSectionElement, options: {id?: string, lineType?: string} = {}) {
|
||||
const tr = document.createElement('tr');
|
||||
if (options.lineType) tr.setAttribute('data-line-type', options.lineType);
|
||||
|
||||
const numberCell = document.createElement('td');
|
||||
numberCell.classList.add('lines-num');
|
||||
const span = document.createElement('span');
|
||||
if (options.id) span.id = options.id;
|
||||
numberCell.append(span);
|
||||
tr.append(numberCell);
|
||||
|
||||
tr.append(document.createElement('td'));
|
||||
tbody.append(tr);
|
||||
return tr;
|
||||
}
|
||||
|
||||
describe('applyDiffLineSelection', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('selects contiguous diff rows, skips expansion rows, and clears previous selection', () => {
|
||||
const fragment = 'diff-selection';
|
||||
|
||||
const otherBox = document.createElement('div');
|
||||
const otherTable = document.createElement('table');
|
||||
otherTable.classList.add('code-diff');
|
||||
const otherTbody = document.createElement('tbody');
|
||||
const staleActiveRow = document.createElement('tr');
|
||||
staleActiveRow.classList.add('active');
|
||||
otherTbody.append(staleActiveRow);
|
||||
otherTable.append(otherTbody);
|
||||
otherBox.append(otherTable);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('diff-file-box');
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('code-diff');
|
||||
const tbody = document.createElement('tbody');
|
||||
table.append(tbody);
|
||||
container.append(table);
|
||||
|
||||
const rows = [
|
||||
createDiffRow(tbody, {id: `${fragment}L1`}),
|
||||
createDiffRow(tbody),
|
||||
createDiffRow(tbody, {lineType: '4'}),
|
||||
createDiffRow(tbody),
|
||||
createDiffRow(tbody, {id: `${fragment}R5`}),
|
||||
createDiffRow(tbody),
|
||||
];
|
||||
|
||||
document.body.append(otherBox, container);
|
||||
|
||||
const range = {fragment, startSide: 'L' as const, startLine: 1, endSide: 'R' as const, endLine: 5};
|
||||
const applied = applyDiffLineSelection(container, range);
|
||||
|
||||
expect(applied).toBe(true);
|
||||
expect(rows[0].classList.contains('active')).toBe(true);
|
||||
expect(rows[1].classList.contains('active')).toBe(true);
|
||||
expect(rows[2].classList.contains('active')).toBe(false);
|
||||
expect(rows[3].classList.contains('active')).toBe(true);
|
||||
expect(rows[4].classList.contains('active')).toBe(true);
|
||||
expect(rows[5].classList.contains('active')).toBe(false);
|
||||
expect(staleActiveRow.classList.contains('active')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when either anchor is missing', () => {
|
||||
const fragment = 'diff-missing';
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('diff-file-box');
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('code-diff');
|
||||
const tbody = document.createElement('tbody');
|
||||
table.append(tbody);
|
||||
container.append(table);
|
||||
document.body.append(container);
|
||||
|
||||
createDiffRow(tbody, {id: `${fragment}L1`});
|
||||
|
||||
const applied = applyDiffLineSelection(container, {fragment, startSide: 'L' as const, startLine: 1, endSide: 'R' as const, endLine: 2});
|
||||
expect(applied).toBe(false);
|
||||
expect(container.querySelectorAll('tr.active').length).toBe(0);
|
||||
});
|
||||
});
|
||||
261
web_src/js/features/repo-diff-selection.ts
Normal file
261
web_src/js/features/repo-diff-selection.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import {addDelegatedEventListener} from '../utils/dom.ts';
|
||||
import {setFileFolding} from './file-fold.ts';
|
||||
|
||||
const diffLineNumberCellSelector = '#diff-file-boxes .code-diff td.lines-num[data-line-num]';
|
||||
const diffAnchorSuffixRegex = /([LR])(\d+)$/;
|
||||
const diffHashRangeRegex = /^(diff-[0-9a-f]+)([LR]\d+)(?:-([LR]\d+))?$/i;
|
||||
export const diffAutoScrollAttr = 'data-auto-scroll-running';
|
||||
|
||||
type DiffAnchorSide = 'L' | 'R';
|
||||
type DiffAnchorInfo = {anchor: string, fragment: string, side: DiffAnchorSide, line: number};
|
||||
type DiffSelectionState = DiffAnchorInfo & {container: HTMLElement};
|
||||
type DiffSelectionRange = {fragment: string, startSide: DiffAnchorSide, startLine: number, endSide: DiffAnchorSide, endLine: number};
|
||||
|
||||
let diffSelectionStart: DiffSelectionState | null = null;
|
||||
|
||||
function scrollDiffAnchorIntoView(targetElement: HTMLElement, currentHash: string) {
|
||||
targetElement.scrollIntoView();
|
||||
document.body.setAttribute(diffAutoScrollAttr, 'true');
|
||||
window.location.hash = '';
|
||||
window.location.hash = currentHash;
|
||||
setTimeout(() => document.body.removeAttribute(diffAutoScrollAttr), 0);
|
||||
}
|
||||
|
||||
function isDiffAnchorId(id: string | null): boolean {
|
||||
return id !== null && id.startsWith('diff-');
|
||||
}
|
||||
|
||||
function parseDiffAnchor(anchor: string): DiffAnchorInfo | null {
|
||||
if (!isDiffAnchorId(anchor)) return null;
|
||||
const suffixMatch = diffAnchorSuffixRegex.exec(anchor);
|
||||
if (!suffixMatch) return null;
|
||||
const line = Number.parseInt(suffixMatch[2]);
|
||||
if (Number.isNaN(line)) return null;
|
||||
const fragment = anchor.slice(0, -suffixMatch[0].length);
|
||||
const side = suffixMatch[1] as DiffAnchorSide;
|
||||
return {anchor, fragment, side, line};
|
||||
}
|
||||
|
||||
export function applyDiffLineSelection(container: HTMLElement, range: DiffSelectionRange): boolean {
|
||||
// Find the start and end anchor elements
|
||||
const startId = `${range.fragment}${range.startSide}${range.startLine}`;
|
||||
const endId = `${range.fragment}${range.endSide}${range.endLine}`;
|
||||
const startSpan = container.querySelector<HTMLElement>(`#${CSS.escape(startId)}`);
|
||||
const endSpan = container.querySelector<HTMLElement>(`#${CSS.escape(endId)}`);
|
||||
|
||||
if (!startSpan || !endSpan) return false;
|
||||
|
||||
const startTr = startSpan.closest('tr');
|
||||
const endTr = endSpan.closest('tr');
|
||||
if (!startTr || !endTr) return false;
|
||||
|
||||
// Clear previous selection
|
||||
for (const tr of document.querySelectorAll('.code-diff tr.active')) {
|
||||
tr.classList.remove('active');
|
||||
}
|
||||
|
||||
// gather rows from the actual table that contains the selection to avoid missing hunks
|
||||
const codeDiffTable = startSpan.closest<HTMLElement>('.code-diff');
|
||||
if (!codeDiffTable || !codeDiffTable.contains(endSpan)) return false;
|
||||
const allRows = Array.from(codeDiffTable.querySelectorAll<HTMLElement>('tbody tr'));
|
||||
const startIndex = allRows.indexOf(startTr);
|
||||
const endIndex = allRows.indexOf(endTr);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1) return false;
|
||||
|
||||
// Select all rows between start and end (inclusive)
|
||||
const minIndex = Math.min(startIndex, endIndex);
|
||||
const maxIndex = Math.max(startIndex, endIndex);
|
||||
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
const row = allRows[i];
|
||||
// Only select rows that are actual diff lines (not comment rows, expansion buttons, etc.)
|
||||
// Skip rows with data-line-type="4" which are code expansion buttons
|
||||
if (row.querySelector('td.lines-num') && row.getAttribute('data-line-type') !== '4') {
|
||||
row.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildDiffHash(range: DiffSelectionRange): string {
|
||||
const startAnchor = `${range.fragment}${range.startSide}${range.startLine}`;
|
||||
if (range.startSide === range.endSide && range.startLine === range.endLine) {
|
||||
return startAnchor;
|
||||
}
|
||||
return `${startAnchor}-${range.endSide}${range.endLine}`;
|
||||
}
|
||||
|
||||
function updateDiffHash(range: DiffSelectionRange) {
|
||||
const hashValue = `#${buildDiffHash(range)}`;
|
||||
if (window.location.hash === hashValue) return;
|
||||
window.history.replaceState(null, '', hashValue);
|
||||
}
|
||||
|
||||
export function parseDiffHashRange(hashValue: string): DiffSelectionRange | null {
|
||||
if (!isDiffAnchorId(hashValue)) return null;
|
||||
const match = diffHashRangeRegex.exec(hashValue);
|
||||
if (!match) return null;
|
||||
const startInfo = parseDiffAnchor(`${match[1]}${match[2]}`);
|
||||
if (!startInfo) return null;
|
||||
let endSide = startInfo.side;
|
||||
let endLine = startInfo.line;
|
||||
if (match[3]) {
|
||||
const endInfo = parseDiffAnchor(`${match[1]}${match[3]}`);
|
||||
if (!endInfo) {
|
||||
return {fragment: startInfo.fragment, startSide: startInfo.side, startLine: startInfo.line, endSide: startInfo.side, endLine: startInfo.line};
|
||||
}
|
||||
endSide = endInfo.side;
|
||||
endLine = endInfo.line;
|
||||
}
|
||||
return {
|
||||
fragment: startInfo.fragment,
|
||||
startSide: startInfo.side,
|
||||
startLine: startInfo.line,
|
||||
endSide,
|
||||
endLine,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitNextAnimationFrame() {
|
||||
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
|
||||
}
|
||||
|
||||
export async function highlightDiffSelectionFromHash(): Promise<boolean> {
|
||||
const {hash} = window.location;
|
||||
if (!hash || !hash.startsWith('#diff-')) return false;
|
||||
const hashValue = hash.substring(1);
|
||||
const range = parseDiffHashRange(hashValue);
|
||||
if (!range) {
|
||||
if (document.body.hasAttribute(diffAutoScrollAttr)) return false;
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
const targetElement = document.getElementById(hashValue);
|
||||
if (!targetElement) return false;
|
||||
scrollDiffAnchorIntoView(targetElement, hash);
|
||||
return true;
|
||||
}
|
||||
const targetId = `${range.fragment}${range.startSide}${range.startLine}`;
|
||||
|
||||
// Wait for the target element to be available (in case it needs to be loaded)
|
||||
let targetSpan = document.querySelector<HTMLElement>(`#${CSS.escape(targetId)}`);
|
||||
if (!targetSpan) {
|
||||
// Flush pending DOM mutations (htmx, folding animations, etc.) before giving up
|
||||
await waitNextAnimationFrame();
|
||||
targetSpan = document.querySelector<HTMLElement>(`#${CSS.escape(targetId)}`);
|
||||
if (!targetSpan) {
|
||||
// Target not found - it might need to be loaded via "show more files"
|
||||
// Return false to let onLocationHashChange handle the loading
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const container = targetSpan.closest<HTMLElement>('.diff-file-box');
|
||||
if (!container) return false;
|
||||
|
||||
// Check if the file is collapsed and expand it if needed
|
||||
if (container.getAttribute('data-folded') === 'true') {
|
||||
const foldBtn = container.querySelector<HTMLElement>('.fold-file');
|
||||
if (foldBtn) {
|
||||
// Expand the file and wait for any transition to finish before selecting lines
|
||||
await setFileFolding(container, foldBtn, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!applyDiffLineSelection(container, range)) return false;
|
||||
updateDiffHash(range);
|
||||
diffSelectionStart = {
|
||||
anchor: targetId,
|
||||
fragment: range.fragment,
|
||||
side: range.startSide,
|
||||
line: range.startLine,
|
||||
container,
|
||||
};
|
||||
|
||||
// Scroll to the first selected line (scroll to the tr element, not the span)
|
||||
// The span is an inline element inside td, we need to scroll to the tr for better visibility
|
||||
await waitNextAnimationFrame();
|
||||
const targetTr = targetSpan.closest('tr');
|
||||
if (targetTr) {
|
||||
targetTr.scrollIntoView({block: 'center'});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleDiffLineNumberClick(cell: HTMLElement, e: MouseEvent) {
|
||||
let span = cell.querySelector<HTMLSpanElement>('span[id^="diff-"]');
|
||||
let info = parseDiffAnchor(span?.id ?? '');
|
||||
|
||||
// If clicked cell has no line number (e.g., clicking on the empty side of a deletion/addition),
|
||||
// try to find the line number from the sibling cell on the same row
|
||||
if (!info) {
|
||||
const row = cell.closest('tr');
|
||||
if (!row) return;
|
||||
// Find the other line number cell in the same row
|
||||
const siblingCell = cell.classList.contains('lines-num-old') ?
|
||||
row.querySelector<HTMLElement>('td.lines-num-new') :
|
||||
row.querySelector<HTMLElement>('td.lines-num-old');
|
||||
if (siblingCell) {
|
||||
span = siblingCell.querySelector<HTMLSpanElement>('span[id^="diff-"]');
|
||||
info = parseDiffAnchor(span?.id ?? '');
|
||||
}
|
||||
if (!info) return;
|
||||
}
|
||||
|
||||
const container = cell.closest<HTMLElement>('.diff-file-box');
|
||||
if (!container) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Check if clicking on a single already-selected line without shift key - deselect it
|
||||
if (!e.shiftKey) {
|
||||
const clickedRow = cell.closest('tr');
|
||||
if (clickedRow?.classList.contains('active')) {
|
||||
// Check if this is a single-line selection by checking if it's the only selected line
|
||||
const selectedRows = container.querySelectorAll('.code-diff tr.active');
|
||||
if (selectedRows.length === 1) {
|
||||
// This is a single selected line, deselect it
|
||||
clickedRow.classList.remove('active');
|
||||
diffSelectionStart = null;
|
||||
// Remove hash from URL completely
|
||||
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
window.getSelection()?.removeAllRanges();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rangeStart: DiffAnchorInfo = info;
|
||||
if (e.shiftKey && diffSelectionStart &&
|
||||
diffSelectionStart.container === container &&
|
||||
diffSelectionStart.fragment === info.fragment) {
|
||||
rangeStart = diffSelectionStart;
|
||||
}
|
||||
|
||||
const range: DiffSelectionRange = {
|
||||
fragment: info.fragment,
|
||||
startSide: rangeStart.side,
|
||||
startLine: rangeStart.line,
|
||||
endSide: info.side,
|
||||
endLine: info.line,
|
||||
};
|
||||
|
||||
if (applyDiffLineSelection(container, range)) {
|
||||
updateDiffHash(range);
|
||||
if (!e.shiftKey || !diffSelectionStart || diffSelectionStart.container !== container || diffSelectionStart.fragment !== info.fragment) {
|
||||
diffSelectionStart = {...info, container};
|
||||
}
|
||||
window.getSelection()?.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
export function initDiffLineSelection() {
|
||||
addDelegatedEventListener<HTMLElement, MouseEvent>(document, 'click', diffLineNumberCellSelector, (cell, e) => {
|
||||
if (e.defaultPrevented) return;
|
||||
handleDiffLineNumberClick(cell, e);
|
||||
});
|
||||
window.addEventListener('hashchange', () => {
|
||||
highlightDiffSelectionFromHash();
|
||||
});
|
||||
highlightDiffSelectionFromHash();
|
||||
}
|
||||
@ -11,6 +11,7 @@ import {createTippy} from '../modules/tippy.ts';
|
||||
import {invertFileFolding} from './file-fold.ts';
|
||||
import {parseDom, sleep} from '../utils.ts';
|
||||
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||
import {parseDiffHashRange, highlightDiffSelectionFromHash, initDiffLineSelection, diffAutoScrollAttr} from './repo-diff-selection.ts';
|
||||
|
||||
function initRepoDiffFileBox(el: HTMLElement) {
|
||||
// switch between "rendered" and "source", for image and CSV files
|
||||
@ -149,7 +150,7 @@ function initDiffHeaderPopup() {
|
||||
}
|
||||
|
||||
// Will be called when the show more (files) button has been pressed
|
||||
function onShowMoreFiles() {
|
||||
async function onShowMoreFiles() {
|
||||
// TODO: replace these calls with the "observer.ts" methods
|
||||
initRepoIssueContentHistory();
|
||||
initViewedCheckboxListenerFor();
|
||||
@ -169,9 +170,11 @@ async function loadMoreFiles(btn: Element): Promise<boolean> {
|
||||
const resp = await response.text();
|
||||
const respDoc = parseDom(resp, 'text/html');
|
||||
const respFileBoxes = respDoc.querySelector('#diff-file-boxes')!;
|
||||
const respFileBoxesChildren = Array.from(respFileBoxes.children); // respFileBoxes.children will be empty after replaceWith
|
||||
// the response is a full HTML page, we need to extract the relevant contents:
|
||||
// * append the newly loaded file list items to the existing list
|
||||
document.querySelector('#diff-incomplete')!.replaceWith(...Array.from(respFileBoxes.children));
|
||||
document.querySelector('#diff-incomplete')!.replaceWith(...respFileBoxesChildren);
|
||||
for (const el of respFileBoxesChildren) window.htmx.process(el);
|
||||
onShowMoreFiles();
|
||||
return true;
|
||||
} catch (error) {
|
||||
@ -219,30 +222,51 @@ function initRepoDiffShowMore() {
|
||||
async function onLocationHashChange() {
|
||||
// try to scroll to the target element by the current hash
|
||||
const currentHash = window.location.hash;
|
||||
if (!currentHash.startsWith('#diff-') && !currentHash.startsWith('#issuecomment-')) return;
|
||||
const issueCommentPrefix = '#issuecomment-';
|
||||
const isDiffHash = currentHash.startsWith('#diff-');
|
||||
const isIssueCommentHash = currentHash.startsWith(issueCommentPrefix);
|
||||
if (!isDiffHash && !isIssueCommentHash) return;
|
||||
|
||||
// avoid reentrance when we are changing the hash to scroll and trigger ":target" selection
|
||||
const attrAutoScrollRunning = 'data-auto-scroll-running';
|
||||
if (document.body.hasAttribute(attrAutoScrollRunning)) return;
|
||||
if (document.body.hasAttribute(diffAutoScrollAttr)) return;
|
||||
|
||||
const targetElementId = currentHash.substring(1);
|
||||
while (currentHash === window.location.hash) {
|
||||
// use getElementById to avoid querySelector throws an error when the hash is invalid
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
const targetElement = document.getElementById(targetElementId);
|
||||
if (targetElement) {
|
||||
// need to change hash to re-trigger ":target" CSS selector, let's manually scroll to it
|
||||
targetElement.scrollIntoView();
|
||||
document.body.setAttribute(attrAutoScrollRunning, 'true');
|
||||
window.location.hash = '';
|
||||
window.location.hash = currentHash;
|
||||
setTimeout(() => document.body.removeAttribute(attrAutoScrollRunning), 0);
|
||||
const hashValue = currentHash.substring(1);
|
||||
let targetElementId = hashValue;
|
||||
|
||||
if (isDiffHash) {
|
||||
const success = await highlightDiffSelectionFromHash();
|
||||
if (success) {
|
||||
// Successfully highlighted and scrolled, we're done
|
||||
return;
|
||||
}
|
||||
const range = parseDiffHashRange(hashValue);
|
||||
if (range) {
|
||||
targetElementId = `${range.fragment}${range.startSide}${range.startLine}`;
|
||||
}
|
||||
}
|
||||
|
||||
while (currentHash === window.location.hash) {
|
||||
if (isDiffHash) {
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
const targetElement = document.getElementById(targetElementId);
|
||||
if (targetElement) {
|
||||
// Try again to highlight and scroll now that the element is loaded
|
||||
const success = await highlightDiffSelectionFromHash();
|
||||
if (success) return;
|
||||
}
|
||||
} else if (isIssueCommentHash) {
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
const commentElement = document.getElementById(hashValue);
|
||||
if (commentElement) {
|
||||
commentElement.scrollIntoView({behavior: 'instant'});
|
||||
window.location.hash = '';
|
||||
window.location.hash = currentHash;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If looking for a hidden comment, try to expand the section that contains it
|
||||
const issueCommentPrefix = '#issuecomment-';
|
||||
if (currentHash.startsWith(issueCommentPrefix)) {
|
||||
if (isIssueCommentHash) {
|
||||
const commentId = currentHash.substring(issueCommentPrefix.length);
|
||||
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${commentId},"]`);
|
||||
if (expandButton) {
|
||||
@ -284,6 +308,7 @@ export function initRepoDiffView() {
|
||||
initDiffHeaderPopup();
|
||||
initViewedCheckboxListenerFor();
|
||||
initExpandAndCollapseFilesButton();
|
||||
initDiffLineSelection();
|
||||
initRepoDiffHashChangeListener();
|
||||
|
||||
registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user