diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index c9aac6d61d..a9323d4778 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -11,7 +11,7 @@
{{else}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
-
|
+ |
{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} |
{{if $line.LeftIdx}}{{end}} |
@@ -27,7 +27,7 @@
{{- end -}}
|
- |
+ |
{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} |
{{if $line.RightIdx}}{{end}} |
@@ -65,8 +65,8 @@
{{if eq .GetType 4}}
| {{$line.RenderBlobExcerptButtons $.FileNameHash $diffBlobExcerptData}} |
{{else}}
- |
- |
+ |
+ |
{{end}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
{{if $inlineDiff.EscapeStatus.Escaped}}{{end}} |
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index ab23b1b934..da5390978a 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -24,7 +24,7 @@
{{$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}}
- |
+ |
{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}{{end}}{{end}} |
|
@@ -39,7 +39,7 @@
{{- end -}}
|
- |
+ |
{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}{{end}}{{end}} |
{{if $match.RightIdx}}{{end}} |
@@ -56,7 +56,7 @@
|
{{else}}
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}}
- |
+ |
{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}{{end}}{{end}} |
{{if $line.LeftIdx}}{{end}} |
@@ -71,7 +71,7 @@
{{- end -}}
|
- |
+ |
{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}{{end}}{{end}} |
{{if $line.RightIdx}}{{end}} |
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index 908b14656e..ddd48b1d84 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -19,8 +19,8 @@
| |
{{end}}
{{else}}
- |
- |
+ |
+ |
{{end}}
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale -}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 9b70e0e6db..1d7fcdbe13 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -985,6 +985,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;
@@ -996,6 +1004,26 @@ 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 {
+ content: "";
+ position: absolute;
+ left: 0;
+ width: 2px;
+ height: 100%;
+ background: var(--color-highlight-fg);
+}
+
.repository .diff-file-box .code-diff-split .tag-code .lines-code code.code-inner {
padding-left: 10px !important;
}
diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts
index 6f5cb2f63b..c688c1826e 100644
--- a/web_src/js/features/repo-diff.ts
+++ b/web_src/js/features/repo-diff.ts
@@ -12,6 +12,130 @@ import {invertFileFolding} from './file-fold.ts';
import {parseDom, sleep} from '../utils.ts';
import {registerGlobalSelectorFunc} from '../modules/observer.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;
+
+type DiffAnchorSide = 'L' | 'R';
+type DiffAnchorInfo = {anchor: string, fragment: string, side: DiffAnchorSide, line: number};
+type DiffSelectionState = DiffAnchorInfo & {container: HTMLElement};
+
+let diffSelectionStart: DiffSelectionState | null = null;
+
+function changeHash(hash: string) {
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+}
+
+function parseDiffAnchor(anchor: string | null): DiffAnchorInfo | null {
+ if (!anchor || !anchor.startsWith('diff-')) 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};
+}
+
+function applyDiffLineSelection(container: HTMLElement, fragment: string, side: DiffAnchorSide, startLine: number, endLine: number, options?: {updateHash?: boolean}): boolean {
+ const minLine = Math.min(startLine, endLine);
+ const maxLine = Math.max(startLine, endLine);
+ const selector = `.code-diff td.lines-num span[id^="${CSS.escape(fragment)}"]`;
+ const spans = Array.from(container.querySelectorAll(selector));
+ const matches = spans.filter((span) => {
+ const info = parseDiffAnchor(span.id);
+ if (!info || info.side !== side) return false;
+ return info.line >= minLine && info.line <= maxLine;
+ });
+ if (!matches.length) return false;
+
+ for (const tr of document.querySelectorAll('.code-diff tr.active')) {
+ tr.classList.remove('active');
+ }
+ for (const span of matches) {
+ span.closest('tr')?.classList.add('active');
+ }
+
+ if (options?.updateHash !== false) {
+ const startAnchor = `${fragment}${side}${minLine}`;
+ const endAnchor = `${fragment}${side}${maxLine}`;
+ const hashValue = minLine === maxLine ? startAnchor : `${startAnchor}-${endAnchor}`;
+ changeHash(`#${hashValue}`);
+ }
+ return true;
+}
+
+type DiffHashRange = {fragment: string, side: DiffAnchorSide, startLine: number, endLine: number};
+
+function parseDiffHashRange(hashValue: string): DiffHashRange | null {
+ if (!hashValue.startsWith('diff-')) return null;
+ const match = diffHashRangeRegex.exec(hashValue);
+ if (!match) return null;
+ const startInfo = parseDiffAnchor(`${match[1]}${match[2]}`);
+ if (!startInfo) return null;
+ let endLine = startInfo.line;
+ if (match[3]) {
+ const endInfo = parseDiffAnchor(`${match[1]}${match[3]}`);
+ if (!endInfo || endInfo.side !== startInfo.side) {
+ return {fragment: startInfo.fragment, side: startInfo.side, startLine: startInfo.line, endLine: startInfo.line};
+ }
+ endLine = endInfo.line;
+ }
+ return {
+ fragment: startInfo.fragment,
+ side: startInfo.side,
+ startLine: startInfo.line,
+ endLine,
+ };
+}
+
+function highlightDiffSelectionFromHash() {
+ const {hash} = window.location;
+ if (!hash || !hash.startsWith('#diff-')) return;
+ const range = parseDiffHashRange(hash.substring(1));
+ if (!range) return;
+ const targetId = `${range.fragment}${range.side}${range.startLine}`;
+ const target = document.querySelector(`#${CSS.escape(targetId)}`);
+ if (!target) return;
+ const container = target.closest('.diff-file-box');
+ if (!container) return;
+ applyDiffLineSelection(container, range.fragment, range.side, range.startLine, range.endLine, {updateHash: false});
+ diffSelectionStart = null;
+}
+
+function handleDiffLineNumberClick(cell: HTMLElement, e: MouseEvent) {
+ const span = cell.querySelector('span[id^="diff-"]');
+ const info = parseDiffAnchor(span?.id ?? null);
+ if (!info) return;
+ const container = cell.closest('.diff-file-box');
+ if (!container) return;
+
+ let rangeStart: DiffAnchorInfo = info;
+ if (e.shiftKey && diffSelectionStart &&
+ diffSelectionStart.container === container &&
+ diffSelectionStart.fragment === info.fragment &&
+ diffSelectionStart.side === info.side) {
+ rangeStart = diffSelectionStart;
+ }
+
+ if (applyDiffLineSelection(container, rangeStart.fragment, rangeStart.side, rangeStart.line, info.line)) {
+ diffSelectionStart = {...info, container};
+ window.getSelection().removeAllRanges();
+ }
+}
+
+function initDiffLineSelection() {
+ addDelegatedEventListener(document, 'click', diffLineNumberCellSelector, (cell, e) => {
+ handleDiffLineNumberClick(cell, e);
+ });
+ window.addEventListener('hashchange', highlightDiffSelectionFromHash);
+ highlightDiffSelectionFromHash();
+}
+
function initRepoDiffFileBox(el: HTMLElement) {
// switch between "rendered" and "source", for image and CSV files
queryElems(el, '.file-view-toggle', (btn) => btn.addEventListener('click', () => {
@@ -283,6 +407,7 @@ export function initRepoDiffView() {
initDiffHeaderPopup();
initViewedCheckboxListenerFor();
initExpandAndCollapseFilesButton();
+ initDiffLineSelection();
initRepoDiffHashChangeListener();
registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
|