diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java index c35fe338dba..4d80b783e12 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java @@ -66,6 +66,7 @@ import org.eclipse.compare.internal.CompareMessages; import org.eclipse.compare.internal.ComparePreferencePage; import org.eclipse.compare.internal.CompareUIPlugin; +import org.eclipse.compare.internal.DiffColors; import org.eclipse.compare.internal.DocumentManager; import org.eclipse.compare.internal.ICompareContextIds; import org.eclipse.compare.internal.ICompareUIConstants; @@ -363,24 +364,9 @@ public ColorPalette(RGB background, String registryKey, RGB defaultRGB) { } else { selected = defaultRGB; } - normal = interpolate(selected, background, 0.6); - fill = interpolate(selected, background, 0.9); - textFill = interpolate(selected, background, 0.8); - } - - private static RGB interpolate(RGB fg, RGB bg, double scale) { - if (fg != null && bg != null) { - return new RGB((int) ((1.0 - scale) * fg.red + scale * bg.red), - (int) ((1.0 - scale) * fg.green + scale * bg.green), - (int) ((1.0 - scale) * fg.blue + scale * bg.blue)); - } - if (fg != null) { - return fg; - } - if (bg != null) { - return bg; - } - return new RGB(128, 128, 128); // a gray + normal = DiffColors.interpolate(selected, background, DiffColors.BORDER_SCALE); + fill = DiffColors.interpolate(selected, background, DiffColors.FILL_SCALE); + textFill = DiffColors.interpolate(selected, background, DiffColors.TEXT_FILL_SCALE); } } diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/DiffColors.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/DiffColors.java new file mode 100644 index 00000000000..564e2e95301 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/DiffColors.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2026 SAP + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SAP - initial implementation + *******************************************************************************/ +package org.eclipse.compare.internal; + +import org.eclipse.swt.graphics.RGB; + +/** + * The shared color palette for diff rendering. Both the traditional two-way + * {@code TextMergeViewer} and the unified diff interpolate the same diff color + * towards the background at the same scales, so the scale factors and the + * interpolation live here once. + */ +public final class DiffColors { + + private DiffColors() { + } + + /** Border / {@code normal} weight: darker and more saturated than the fill. */ + public static final double BORDER_SCALE = 0.6; + + /** The detailed (word-level) diff text background. */ + public static final double TEXT_FILL_SCALE = 0.8; + + /** The hunk fill band; the lightest of the three. */ + public static final double FILL_SCALE = 0.9; + + /** Border color of an addition/deletion hunk; darker than the fill band. */ + public static RGB borderColor(RGB diffColor, RGB background) { + return interpolate(diffColor, background, BORDER_SCALE); + } + + /** + * Linearly interpolates between {@code fg} and {@code bg}: scale 0 returns + * {@code fg}, scale 1 returns {@code bg}. + */ + public static RGB interpolate(RGB fg, RGB bg, double scale) { + if (fg != null && bg != null) { + return new RGB((int) ((1.0 - scale) * fg.red + scale * bg.red), + (int) ((1.0 - scale) * fg.green + scale * bg.green), + (int) ((1.0 - scale) * fg.blue + scale * bg.blue)); + } + if (fg != null) { + return fg; + } + if (bg != null) { + return bg; + } + return new RGB(128, 128, 128); // a gray + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffCodeMiningProvider.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffCodeMiningProvider.java index ef6b46aa38f..c7ff124dabf 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffCodeMiningProvider.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffCodeMiningProvider.java @@ -34,6 +34,7 @@ import java.util.function.Consumer; import org.eclipse.compare.internal.CompareMessages; +import org.eclipse.compare.internal.DiffColors; import org.eclipse.compare.unifieddiff.UnifiedDiffMode; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; import org.eclipse.core.runtime.IProgressMonitor; @@ -87,6 +88,7 @@ public class UnifiedDiffCodeMiningProvider extends AbstractCodeMiningProvider { private Color deletionBackgroundColor; private Color detailedDiffColor; + private Color borderColor; private Color foldSeparatorColor; private Color foldButtonColor; private boolean lastIsOverlay; @@ -102,6 +104,10 @@ public void dispose() { detailedDiffColor.dispose(); } detailedDiffColor = null; + if (borderColor != null && !borderColor.isDisposed()) { + borderColor.dispose(); + } + borderColor = null; if (foldSeparatorColor != null && !foldSeparatorColor.isDisposed()) { foldSeparatorColor.dispose(); } @@ -141,6 +147,9 @@ public CompletableFuture> provideCodeMinings(ITextVi if (this.deletionBackgroundColor != null && !this.deletionBackgroundColor.isDisposed()) { this.deletionBackgroundColor.dispose(); } + if (this.borderColor != null && !this.borderColor.isDisposed()) { + this.borderColor.dispose(); + } if (this.foldSeparatorColor != null && !this.foldSeparatorColor.isDisposed()) { this.foldSeparatorColor.dispose(); } @@ -148,8 +157,9 @@ public CompletableFuture> provideCodeMinings(ITextVi this.foldButtonColor.dispose(); } // the word-level diff is tinted stronger so it stands out against the band - this.detailedDiffColor = new Color(interpolate(deletionColor, background, 0.8)); - this.deletionBackgroundColor = new Color(interpolate(deletionColor, background, 0.9)); + this.detailedDiffColor = new Color(interpolate(deletionColor, background, DiffColors.TEXT_FILL_SCALE)); + this.deletionBackgroundColor = new Color(interpolate(deletionColor, background, DiffColors.FILL_SCALE)); + this.borderColor = new Color(UnifiedDiffText.borderColor(deletionColor, background)); this.foldSeparatorColor = new Color(separatorBackground(background)); this.foldButtonColor = new Color(buttonBackground(background)); lastIsOverlay = isOverlay; @@ -318,13 +328,13 @@ private ICodeMining createMining(IDocument doc, UnifiedDiff diff, int offset, in throws BadLocationException { int end = doc.getLength(); if (offset >= end && !startsLine(doc, end)) { - return new UnifiedDiffFooterCodeMining(doc, this, diff, tabWidth, this.deletionBackgroundColor, this.detailedDiffColor, tv); + return new UnifiedDiffFooterCodeMining(doc, this, diff, tabWidth, this.deletionBackgroundColor, this.detailedDiffColor, this.borderColor, tv); } // a position must not reach beyond the document, otherwise the annotation model // silently drops it int start = Math.min(offset, end); return new UnifiedDiffLineHeaderCodeMining(new Position(start, start < end ? 1 : 0), this, diff, tabWidth, - this.deletionBackgroundColor, this.detailedDiffColor, tv); + this.deletionBackgroundColor, this.detailedDiffColor, this.borderColor, tv); } private static boolean startsLine(IDocument doc, int offset) throws BadLocationException { @@ -498,6 +508,18 @@ interface IUnifiedDiffCodeMining { String getLabel(); } + /** Draws the thin top and bottom border of a code-mining band. */ + private static void drawBandBorder(GC gc, Color borderColor, int y, int width, int height) { + if (borderColor == null || borderColor.isDisposed() || width <= 0 || height <= 0) { + return; + } + Color previousBackground = gc.getBackground(); + gc.setBackground(borderColor); + gc.fillRectangle(0, y, width, 1); // top + gc.fillRectangle(0, y + height - 1, width, 1); // bottom + gc.setBackground(previousBackground); + } + static class MouseClickConsumer implements Consumer { private final ITextViewer viewer; @@ -538,6 +560,7 @@ public static class UnifiedDiffFooterCodeMining extends DocumentFooterCodeMining private final String unifiedDiffLabel; private final Color deletionBackgroundColor; private final Color detailedDiffColor; + private final Color borderColor; private final int tabWidth; private final ITextViewer viewer; private UnifiedDiff diff; @@ -548,10 +571,11 @@ public static class UnifiedDiffFooterCodeMining extends DocumentFooterCodeMining public UnifiedDiffFooterCodeMining(IDocument document, ICodeMiningProvider provider, UnifiedDiff diff, int tabWidth, Color deletionBackgroundColor, Color detailedDiffColor, - ITextViewer viewer) { + Color borderColor, ITextViewer viewer) { super(document, provider, new MouseClickConsumer(viewer)); this.deletionBackgroundColor = deletionBackgroundColor; this.detailedDiffColor = detailedDiffColor; + this.borderColor = borderColor; this.tabWidth = tabWidth; this.viewer = viewer; if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { @@ -642,6 +666,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { if (ranges.isEmpty()) { // no syntax coloring available; fall back to plain rendering result = super.draw(gc, textWidget, color, x, y); + drawBandBorder(gc, this.borderColor, y, textWidget.getBounds().width, lastRectangle.height); return result; } @@ -650,6 +675,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { fillDetailedDiffBackgrounds(gc, textWidget, label, ranges, x, y); gc.setFont(font); drawStyleRanges(gc, textWidget, ranges, label, styledFonts, x, y, null); + drawBandBorder(gc, this.borderColor, y, textWidget.getBounds().width, lastRectangle.height); return result; } @@ -895,6 +921,7 @@ public static class UnifiedDiffLineHeaderCodeMining extends LineHeaderCodeMining private final String unifiedDiffLabel; private final Color deletionBackgroundColor; private final Color detailedDiffColor; + private final Color borderColor; private final UnifiedDiff diff; private final int tabWidth; private ITextViewer viewer; @@ -909,8 +936,8 @@ public static class UnifiedDiffLineHeaderCodeMining extends LineHeaderCodeMining private final HashMap> styledFonts = new HashMap<>(); public UnifiedDiffLineHeaderCodeMining(Position position, ICodeMiningProvider provider, UnifiedDiff diff, - int tabWidth, Color deletionBackgroundColor, Color detailedDiffColor, ITextViewer viewer) - throws BadLocationException { + int tabWidth, Color deletionBackgroundColor, Color detailedDiffColor, Color borderColor, + ITextViewer viewer) throws BadLocationException { super(position, provider, new MouseClickConsumer(viewer)); if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { this.unifiedDiffLabel = removeTrailingNewLines(replaceTabWithSpaces(diff.leftStr, tabWidth)); @@ -919,6 +946,7 @@ public UnifiedDiffLineHeaderCodeMining(Position position, ICodeMiningProvider pr } this.deletionBackgroundColor = deletionBackgroundColor; this.detailedDiffColor = detailedDiffColor; + this.borderColor = borderColor; this.diff = diff; this.tabWidth = tabWidth; this.viewer = viewer; @@ -1075,6 +1103,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { gc.drawString(f.str(), x + f.x(), y + f.y(), true); } if (!fontIsDisposed) { + drawBandBorder(gc, this.borderColor, y, textWidget.getBounds().width, lastRectangle.height); lastRectangle = new Rectangle(x, y, lastRectangle.width, lastRectangle.height); return new Point(lastRectangle.width, lastRectangle.height); } @@ -1208,11 +1237,13 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { // still rendered. backgrounds/foregrounds are intentionally left unset // so the next paint takes the full slow path again. result = super.draw(gc, textWidget, color, x, y); + drawBandBorder(gc, this.borderColor, y, textWidget.getBounds().width, result.y); return result; } foregrounds = new ArrayList<>(); gc.setFont(cachedFont); drawStyleRanges(gc, textWidget, ranges, label, styledFonts, x, y, foregrounds::add); + drawBandBorder(gc, this.borderColor, y, textWidget.getBounds().width, lastRectangle.height); return result; } @@ -1419,17 +1450,6 @@ static StyleRange transformFontStyleToFont(Map> styledF // from inner class ColorPalette in TextMergeViewer static RGB interpolate(RGB fg, RGB bg, double scale) { - if (fg != null && bg != null) { - return new RGB((int) ((1.0 - scale) * fg.red + scale * bg.red), - (int) ((1.0 - scale) * fg.green + scale * bg.green), - (int) ((1.0 - scale) * fg.blue + scale * bg.blue)); - } - if (fg != null) { - return fg; - } - if (bg != null) { - return bg; - } - return new RGB(128, 128, 128); // a gray + return UnifiedDiffText.interpolate(fg, bg, scale); } } diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java index f86c9762e71..0e1e0f17a6b 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java @@ -94,6 +94,7 @@ import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; @@ -1758,6 +1759,7 @@ private static class UnifiedDiffPaintListener implements PaintListener { private final IAnnotationModel model; private final ITextViewer viewer; private final Color additionBackgroundColor; + private final Color borderColor; private final StyledText w; public UnifiedDiffPaintListener(ITextViewer viewer, IAnnotationModel model, UnifiedDiffMode mode) { @@ -1773,12 +1775,16 @@ public UnifiedDiffPaintListener(ITextViewer viewer, IAnnotationModel model, Unif RGB background = UnifiedDiffCodeMiningProvider.getBackground(); RGB interpolated = UnifiedDiffCodeMiningProvider.interpolate(color, background, 0.9); this.additionBackgroundColor = new Color(interpolated); + this.borderColor = new Color(UnifiedDiffText.borderColor(color, background)); } void dispose() { if (additionBackgroundColor != null && !additionBackgroundColor.isDisposed()) { additionBackgroundColor.dispose(); } + if (borderColor != null && !borderColor.isDisposed()) { + borderColor.dispose(); + } } @Override @@ -1810,11 +1816,19 @@ public void paintControl(PaintEvent e) { } posLength = pos.length; } - int fromLine = Math.max(this.w.getLineAtOffset(posOffset), firstDamagedLine); - int toLine = Math.min(this.w.getLineAtOffset(posOffset + posLength), lastDamagedLine + 1); + int hunkFirstLine = this.w.getLineAtOffset(posOffset); + // a band ending on a line delimiter belongs to the line it is on, not to + // the next one the delimiter starts; a zero-length band paints nothing + int lastLine = posLength > 0 ? this.w.getLineAtOffset(posOffset + posLength - 1) : hunkFirstLine - 1; + int fromLine = Math.max(hunkFirstLine, firstDamagedLine); + int toLine = Math.min(lastLine + 1, lastDamagedLine + 1); if (fromLine >= toLine) { continue; } + // A companion mining, when present, abuts this band and paints the same + // shared edge in the same color, so drawing our own top/bottom here is at + // worst a harmless repaint of one pixel row - and the only way to guarantee + // the edge is never missing when no mining is adjacent. e.gc.setBackground(this.additionBackgroundColor); for (int lineNr = fromLine; lineNr < toLine; lineNr++) { String line = this.w.getLine(lineNr); @@ -1837,9 +1851,27 @@ public void paintControl(PaintEvent e) { // need to draw it by our own e.gc.fillRectangle(2, endLineBounds.y + endLineBounds.height - this.w.getLineSpacing(), endLineBounds.x + endLineBounds.width, this.w.getLineSpacing()); + + drawBandBorder(e.gc, bounds.width, endLineBounds.y, endLineBounds.height, + lineNr == hunkFirstLine, lineNr == lastLine); + e.gc.setBackground(this.additionBackgroundColor); } } } + + /** Draws the thin top/bottom edge of the hunk band on its first/last line. */ + private void drawBandBorder(GC gc, int width, int y, int height, boolean top, boolean bottom) { + if (this.borderColor == null || this.borderColor.isDisposed() || width <= 0 || height <= 0) { + return; + } + gc.setBackground(this.borderColor); + if (top) { + gc.fillRectangle(0, y, width, 1); + } + if (bottom) { + gc.fillRectangle(0, y + height - 1, width, 1); + } + } } private static boolean validateEdit(IFile file) { diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffText.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffText.java index 2c7b996aa2b..1828c67eb0c 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffText.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffText.java @@ -16,8 +16,10 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.compare.internal.DiffColors; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; /** * Side-effect free text and style-range helpers used to lay out the unified @@ -28,6 +30,25 @@ public final class UnifiedDiffText { private UnifiedDiffText() { } + /** Border color scale; matches {@code TextMergeViewer}'s {@code normal} color. */ + public static final double BORDER_SCALE = DiffColors.BORDER_SCALE; + + /** Fill band color scale. */ + public static final double FILL_SCALE = DiffColors.FILL_SCALE; + + /** Border color of an addition/deletion hunk; darker than the fill band. */ + public static RGB borderColor(RGB diffColor, RGB background) { + return DiffColors.borderColor(diffColor, background); + } + + /** + * Linearly interpolates between {@code fg} and {@code bg}: scale 0 returns + * {@code fg}, scale 1 returns {@code bg}. + */ + public static RGB interpolate(RGB fg, RGB bg, double scale) { + return DiffColors.interpolate(fg, bg, scale); + } + /** * Returns the number of lines of the given string, counting a trailing line * delimiter as starting a new (empty) line. An empty string is one line. diff --git a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffCodeMiningProviderTest.java b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffCodeMiningProviderTest.java index 3b088d8add0..3949e0b0f55 100644 --- a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffCodeMiningProviderTest.java +++ b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffCodeMiningProviderTest.java @@ -434,8 +434,9 @@ public void testFooterMiningDetailedDiffUsesStyledTextPosition() throws Exceptio Color deletionColor = new Color(display, 11, 22, 33); Color detailedDiffColor = new Color(display, 44, 55, 66); + Color borderColor = new Color(display, 77, 88, 99); UnifiedDiffFooterCodeMining paintingFooter = new UnifiedDiffFooterCodeMining(document, provider, - footer.getUnifiedDiff(), 4, deletionColor, detailedDiffColor, viewer); + footer.getUnifiedDiff(), 4, deletionColor, detailedDiffColor, borderColor, viewer); Image image = new Image(display, 1200, 100); GC gc = new GC(image); Font boldFont = null; @@ -472,6 +473,7 @@ public void testFooterMiningDetailedDiffUsesStyledTextPosition() throws Exceptio } deletionColor.dispose(); detailedDiffColor.dispose(); + borderColor.dispose(); } } @@ -500,8 +502,9 @@ public void testFooterMiningMultiLineDetailedDiffResetsPerLine() throws Exceptio Color deletionColor = new Color(display, 11, 22, 33); Color detailedDiffColor = new Color(display, 44, 55, 66); + Color borderColor = new Color(display, 77, 88, 99); UnifiedDiffFooterCodeMining paintingFooter = new UnifiedDiffFooterCodeMining(document, provider, - footer.getUnifiedDiff(), 4, deletionColor, detailedDiffColor, viewer); + footer.getUnifiedDiff(), 4, deletionColor, detailedDiffColor, borderColor, viewer); Image image = new Image(display, 400, 200); GC gc = new GC(image); try { @@ -534,6 +537,7 @@ public void testFooterMiningMultiLineDetailedDiffResetsPerLine() throws Exceptio paintingFooter.dispose(); deletionColor.dispose(); detailedDiffColor.dispose(); + borderColor.dispose(); } } diff --git a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffTextTest.java b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffTextTest.java index b448af69b7d..4b51f564a0f 100644 --- a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffTextTest.java +++ b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffTextTest.java @@ -13,13 +13,16 @@ *******************************************************************************/ package org.eclipse.team.tests.ui; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.borderColor; import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.clampDetailedDiffLength; import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.countLines; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.interpolate; import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.mapOffsetToTabExpanded; import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.mergeStyleRanges; import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffText.replaceTabWithSpaces; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,6 +36,7 @@ import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffLineHeaderCodeMining; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffLineHeaderCodeMining.RangeInfo; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffText; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.Position; import org.eclipse.swt.SWT; @@ -41,6 +45,7 @@ import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.junit.jupiter.api.AfterEach; @@ -302,6 +307,27 @@ public void testDetailedDiffRunningIntoTrailingSpacesIsTrimmedNotDropped() { assertEquals(3, ranges.get(0).length, "highlight is trimmed to 'bar', dropping the trailing spaces"); } + // ------------------------------------------- borderColor + + /** + * The hunk border reuses the traditional viewer's {@code normal} weight: it is + * {@code interpolate(diffColor, background, 0.6)}, darker and more saturated + * than the {@code 0.9} fill band, so the two must never collapse to the same + * color on either a light or a dark background. + */ + @Test + public void testBorderColorMatchesInterpolateAndDiffersFromFill() { + RGB diff = new RGB(220, 60, 60); + for (RGB background : new RGB[] { new RGB(255, 255, 255), new RGB(30, 30, 30) }) { + RGB border = borderColor(diff, background); + RGB fill = interpolate(diff, background, UnifiedDiffText.FILL_SCALE); + assertAll( // + () -> assertEquals(interpolate(diff, background, UnifiedDiffText.BORDER_SCALE), border, + "border is the 0.6 interpolation"), // + () -> assertNotEquals(fill, border, "border must be distinct from the 0.9 fill band")); + } + } + // ------------------------------------------- getPositionForOffset @Test @@ -318,7 +344,7 @@ public void testGetPositionForOffsetResetsXAfterNewlineAtRangeStart() throws Exc UnifiedDiff diff = new UnifiedDiff(doc, 0, str.length(), str, doc, 0, str.length(), str, List.of(), UnifiedDiffMode.REPLACE_MODE); UnifiedDiffLineHeaderCodeMining mining = new UnifiedDiffLineHeaderCodeMining( - new Position(0, 1), null, diff, 4, null, null, null); + new Position(0, 1), null, diff, 4, null, null, null, null); gc.setFont(styledText.getFont()); Point result = mining.getPositionForOffset(styledText, gc, 7, str, ranges, new RangeInfo(-1, -1, null)); @@ -331,21 +357,6 @@ public void testGetPositionForOffsetResetsXAfterNewlineAtRangeStart() throws Exc // ------------------------------------------------------------------ helpers - private static UnifiedDiff replaceDiff(String diffStr, UnifiedDiff... detailedDiffs) { - Document doc = new Document(diffStr); - UnifiedDiff diff = new UnifiedDiff(doc, 0, diffStr.length(), diffStr, doc, 0, diffStr.length(), diffStr, - new ArrayList<>(), UnifiedDiffMode.REPLACE_MODE); - Collections.addAll(diff.detailedDiffs, detailedDiffs); - return diff; - } - - private static UnifiedDiff detailedDiff(String diffStr, int start, int length) { - Document doc = new Document(diffStr); - String sub = diffStr.substring(start, start + length); - return new UnifiedDiff(doc, start, start + length, sub, doc, start, start + length, sub, List.of(), - UnifiedDiffMode.REPLACE_MODE); - } - /** * The merged ranges are handed to {@code StyledText#setStyleRanges}, which * rejects overlapping ranges. They must therefore tile the foregrounds exactly: @@ -416,6 +427,21 @@ private static Color systemColor(int id) { return Display.getDefault().getSystemColor(id); } + private static UnifiedDiff replaceDiff(String diffStr, UnifiedDiff... detailedDiffs) { + Document doc = new Document(diffStr); + UnifiedDiff diff = new UnifiedDiff(doc, 0, diffStr.length(), diffStr, doc, 0, diffStr.length(), diffStr, + new ArrayList<>(), UnifiedDiffMode.REPLACE_MODE); + Collections.addAll(diff.detailedDiffs, detailedDiffs); + return diff; + } + + private static UnifiedDiff detailedDiff(String diffStr, int start, int length) { + Document doc = new Document(diffStr); + String sub = diffStr.substring(start, start + length); + return new UnifiedDiff(doc, start, start + length, sub, doc, start, start + length, sub, List.of(), + UnifiedDiffMode.REPLACE_MODE); + } + private static StyleRange styledRange(int start, int length) { StyleRange range = new StyleRange(); range.start = start;