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 c3a48f41c21..e6e4cc5e578 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 @@ -316,7 +316,7 @@ 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, null, diff, tabWidth, this.deletionBackgroundColor); + return new UnifiedDiffFooterCodeMining(doc, this, diff, tabWidth, this.deletionBackgroundColor, tv); } // a position must not reach beyond the document, otherwise the annotation model // silently drops it @@ -487,21 +487,92 @@ private static void drawChevrons(GC gc, int centerX, int centerY, int height, bo } } - static class UnifiedDiffFooterCodeMining extends DocumentFooterCodeMining { + interface IUnifiedDiffCodeMining { + Rectangle getLastRectangle(); + Color getDeletionBackgroundColor(); + UnifiedDiff getUnifiedDiff(); + String getLabel(); + /** Returns detailed diff background ranges, or an empty list if not applicable. */ + List createDetailedDiffBackgroundRanges(String txt); + } + + static class MouseClickConsumer implements Consumer { + + private final ITextViewer viewer; + private IUnifiedDiffCodeMining mining; + + public MouseClickConsumer(ITextViewer viewer) { + this.viewer = viewer; + } + + public void setCodeMining(IUnifiedDiffCodeMining mining) { + this.mining = mining; + } + + @Override + public void accept(MouseEvent t) { + if (mining == null || viewer == null || mining.getLastRectangle() == null) { + return; + } + StyledText st = viewer.getTextWidget(); + StyledText overlay = new StyledText(st, SWT.NONE); + overlay.setBounds(mining.getLastRectangle()); + overlay.setFont(st.getFont()); + overlay.setBackground(mining.getDeletionBackgroundColor()); + overlay.setLineSpacing(st.getLineSpacing()); + String txt = mining.getLabel().stripTrailing(); + overlay.setText(txt); + overlay.setFocus(); + List backgrounds = mining.createDetailedDiffBackgroundRanges(txt); + List foregrounds = computeStyleRanges(viewer, mining.getUnifiedDiff().leftStart, txt); + List ranges = mergeStyleRanges(backgrounds, foregrounds); + overlay.setStyleRanges(ranges.toArray(new StyleRange[] {})); + openOverlay(overlay, viewer); + } + } + + public static class UnifiedDiffFooterCodeMining extends DocumentFooterCodeMining implements IUnifiedDiffCodeMining { private final String unifiedDiffLabel; private final Color deletionBackgroundColor; + private final ITextViewer viewer; private UnifiedDiff diff; + private List styleRanges; + private final HashMap> styledFonts = new HashMap<>(); + private Rectangle lastRectangle; + private Font cachedFont; public UnifiedDiffFooterCodeMining(IDocument document, ICodeMiningProvider provider, - Consumer action, UnifiedDiff diff, int tabWidth, Color deletionBackgroundColor) { - super(document, provider, action); + UnifiedDiff diff, int tabWidth, Color deletionBackgroundColor, ITextViewer viewer) { + super(document, provider, new MouseClickConsumer(viewer)); this.deletionBackgroundColor = deletionBackgroundColor; + this.viewer = viewer; if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { this.unifiedDiffLabel = replaceTabWithSpaces(diff.leftStr, tabWidth); } else { this.unifiedDiffLabel = replaceTabWithSpaces(diff.rightStr, tabWidth); } this.diff = diff; + ((MouseClickConsumer) getAction()).setCodeMining(this); + } + + @Override + public Rectangle getLastRectangle() { + return lastRectangle; + } + + @Override + public Color getDeletionBackgroundColor() { + return deletionBackgroundColor; + } + + @Override + public UnifiedDiff getUnifiedDiff() { + return diff; + } + + @Override + public List createDetailedDiffBackgroundRanges(String txt) { + return new ArrayList<>(); } @Override @@ -509,8 +580,25 @@ public String getLabel() { return this.unifiedDiffLabel; } - public UnifiedDiff getUnifiedDiff() { - return this.diff; + @Override + public void dispose() { + styleRanges = null; + lastRectangle = null; + cachedFont = null; + clearStyledFonts(); + super.dispose(); + } + + private void clearStyledFonts() { + styledFonts.forEach((font, map) -> map.forEach((style, f) -> f.dispose())); + styledFonts.clear(); + } + + private List styleRanges(String label) { + if (styleRanges == null) { + styleRanges = computeStyleRanges(viewer, diff.leftStart, label); + } + return styleRanges; } @Override @@ -520,27 +608,130 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { gc.setForeground(c); Font font = textWidget.getFont(); gc.setFont(font); + if (cachedFont != null && (cachedFont.isDisposed() || !cachedFont.equals(font))) { + // font might have been changed in the meantime - drop the derived fonts + // keyed on the old base font so their native handles are not leaked + clearStyledFonts(); + } + cachedFont = font; // first run to get width and height for label // change from https://github.com/eclipse-platform/eclipse.platform.ui/pull/3651 // is required so that background correctly drawn with line spacing > 0 Point result = super.draw(gc, textWidget, color, x, y); + lastRectangle = new Rectangle(x, y, result.x, result.y); // draw background // vs code is drawing the background to the top right of the editor - we do here // the same! gc.fillRectangle(0, y, textWidget.getBounds().width /* result.x */, result.y); - // draw foreground again - result = super.draw(gc, textWidget, color, x, y); + + String label = getLabel(); + List ranges = styleRanges(label); + if (ranges.isEmpty()) { + // no syntax coloring available; fall back to plain rendering + result = super.draw(gc, textWidget, color, x, y); + return result; + } + + gc.setFont(font); + drawStyleRanges(gc, textWidget, ranges, label, styledFonts, x, y, null); return result; } } + record ForegroundInfo(int x, int y, String str, Font font, Color background, Color foreground) { + } + + /** + * Draws the syntax-colored label using the given style ranges, advancing the + * cursor position range by range. The {@code onForeground} consumer is called + * for each drawn segment and may be {@code null}; the header mining uses it to + * populate its foreground cache so subsequent repaints skip this path. + */ + static void drawStyleRanges(GC gc, StyledText textWidget, List ranges, String label, + HashMap> styledFonts, int x, int y, + Consumer onForeground) { + Font font = gc.getFont(); + int textWidgetLineHeight = textWidget.getLineHeight(); + int cx = x; + int cy = y; + for (StyleRange range : ranges) { + String sub = label.substring(range.start, range.start + range.length); + if (sub.trim().length() > 0) { + if (range.background != null) { + gc.setBackground(range.background); + } + if (range.foreground != null) { + gc.setForeground(range.foreground); + } + Font currentFont = gc.getFont(); + var rangeWithFont = transformFontStyleToFont(styledFonts, currentFont, range); + if (rangeWithFont.font != null) { + gc.setFont(rangeWithFont.font); + } + String[] lines = sub.split("\n"); //$NON-NLS-1$ + if (lines.length > 1) { + for (int i = 0; i < lines.length; i++) { + String line = lines[i].replace("\r", ""); //$NON-NLS-1$ //$NON-NLS-2$ + gc.drawString(line, cx, cy, true); + if (onForeground != null) { + onForeground.accept(new ForegroundInfo(cx - x, cy - y, line, gc.getFont(), + gc.getBackground(), gc.getForeground())); + } + Point p = gc.stringExtent(line); + if (i < lines.length - 1) { + cy += textWidgetLineHeight + textWidget.getLineSpacing(); + cx = x; + } else { + if (sub.endsWith("\n")) { //$NON-NLS-1$ + cy += textWidgetLineHeight + textWidget.getLineSpacing(); + cx = x; + } else { + cx += p.x; + } + } + } + } else { + gc.drawString(sub, cx, cy, true); + if (onForeground != null) { + onForeground.accept(new ForegroundInfo(cx - x, cy - y, sub, gc.getFont(), + gc.getBackground(), gc.getForeground())); + } + Point p = gc.stringExtent(sub); + if (sub.endsWith("\n")) { //$NON-NLS-1$ + cy += textWidgetLineHeight + textWidget.getLineSpacing(); + cx = x; + } else { + cx += p.x; + } + } + gc.setFont(currentFont); + } else { + int lfCount = 0; + if (sub.contains("\n")) { //$NON-NLS-1$ + lfCount = sub.split("\n", -1).length - 1; //$NON-NLS-1$ + sub = sub.substring(sub.lastIndexOf("\n") + 1); //$NON-NLS-1$ + } + Point p = gc.stringExtent(sub); + if (lfCount > 0) { + cy += lfCount * (textWidgetLineHeight + textWidget.getLineSpacing()); + cx = x; + } + cx += p.x; + } + } + gc.setFont(font); + } + private static List computeStyleRanges(ITextViewer v, int offset, String source) { List result = new ArrayList<>(); if (!(v instanceof SourceViewer sv)) { return result; } + IDocument originalDocument = sv.getDocument(); + if (originalDocument == null) { + return result; + } try { - IDocument originalDocument = sv.getDocument(); String prefix = originalDocument.get(0, offset /* diff.leftStart */); IDocument document = new Document(prefix + source); IRegion damage = new Region(prefix.length(), source.length()); @@ -559,7 +750,7 @@ private static List computeStyleRanges(ITextViewer v, int offset, St } } - public static class UnifiedDiffLineHeaderCodeMining extends LineHeaderCodeMining { + public static class UnifiedDiffLineHeaderCodeMining extends LineHeaderCodeMining implements IUnifiedDiffCodeMining { private final String unifiedDiffLabel; private final Color deletionBackgroundColor; private final Color detailedDiffColor; @@ -631,153 +822,66 @@ private static List computeDetailedDiffRanges(UnifiedDiff dif return result; } - private static final class ForegroundInfo { - - final int x; - final int y; - final String str; - final Font font; - final Color background; - final Color foreground; - - public ForegroundInfo(int x, int y, String str, Font font, Color background, Color foreground) { - this.x = x; - this.y = y; - this.str = str; - this.font = font; - this.background = background; - this.foreground = foreground; - } - + @Override + public String getLabel() { + return this.unifiedDiffLabel; } - private static class MouseClickConsumer implements Consumer { - - private final ITextViewer viewer; - private UnifiedDiffLineHeaderCodeMining mining; + @Override + public UnifiedDiff getUnifiedDiff() { + return this.diff; + } - public MouseClickConsumer(ITextViewer viewer) { - this.viewer = viewer; - } + @Override + public Rectangle getLastRectangle() { + return lastRectangle; + } - public void setCodeMining(UnifiedDiffLineHeaderCodeMining mining) { - this.mining = mining; - } + @Override + public Color getDeletionBackgroundColor() { + return deletionBackgroundColor; + } - @Override - public void accept(MouseEvent t) { - if (mining == null || viewer == null || mining.lastRectangle == null) { - return; + @Override + public List createDetailedDiffBackgroundRanges(String txt) { + List ranges = new ArrayList<>(); + String diffStr = diff.mode.equals(UnifiedDiffMode.REPLACE_MODE) ? diff.leftStr : diff.rightStr; + String trimmedDiffStr = removeTrailingNewLines(diffStr); + for (var detailedDiff : diff.detailedDiffs) { + int detailedDiffStart; + int detailedDiffLength; + String detailedDiffStr; + if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { + detailedDiffStart = detailedDiff.leftStart; + detailedDiffLength = detailedDiff.leftLength; + detailedDiffStr = detailedDiff.leftStr; + } else { + detailedDiffStart = detailedDiff.rightStart; + detailedDiffLength = detailedDiff.rightLength; + detailedDiffStr = detailedDiff.rightStr; } - StyledText st = viewer.getTextWidget(); - StyledText overlay = new StyledText(st, SWT.NONE); - overlay.setBounds(mining.lastRectangle); - overlay.setFont(st.getFont()); - overlay.setBackground(mining.deletionBackgroundColor); - overlay.setLineSpacing(st.getLineSpacing()); - String txt = mining.getLabel().stripTrailing(); - overlay.setText(txt); - overlay.setFocus(); - List backgrounds = createDetailedDiffBackgroundRanges(mining, txt); - List foregrounds = computeStyleRanges(viewer, mining.diff.leftStart, txt); - List ranges = mergeStyleRanges(backgrounds, foregrounds); - overlay.setStyleRanges(ranges.toArray(new StyleRange[] {})); - overlay.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - overlay.dispose(); - setTextEditorActionsActivated(true); - } - }); - overlay.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.keyCode == SWT.ESC) { - overlay.dispose(); - setTextEditorActionsActivated(true); - } - e.doit = false; - } - }); - setTextEditorActionsActivated(false); - } - - private List createDetailedDiffBackgroundRanges(UnifiedDiffLineHeaderCodeMining miningParam, - String txt) { - List ranges = new ArrayList<>(); - String diffStr = miningParam.diff.mode.equals(UnifiedDiffMode.REPLACE_MODE) ? miningParam.diff.leftStr - : miningParam.diff.rightStr; - String trimmedDiffStr = removeTrailingNewLines(diffStr); - for (var detailedDiff : miningParam.diff.detailedDiffs) { - int detailedDiffStart; - int detailedDiffLength; - String detailedDiffStr; - if (miningParam.diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { - detailedDiffStart = detailedDiff.leftStart; - detailedDiffLength = detailedDiff.leftLength; - detailedDiffStr = detailedDiff.leftStr; - } else { - detailedDiffStart = detailedDiff.rightStart; - detailedDiffLength = detailedDiff.rightLength; - detailedDiffStr = detailedDiff.rightStr; - } - if (detailedDiffStr.trim().length() == 0) { - continue; - } - if (detailedDiffStart + detailedDiffLength >= trimmedDiffStr.length()) { - int delta = diffStr.length() - trimmedDiffStr.length(); - if (detailedDiffLength <= delta) { - continue; - } - detailedDiffLength -= delta; - } - int expandedStart = mapOffsetToTabExpanded(diffStr, detailedDiffStart, miningParam.tabWidth); - int expandedEnd = mapOffsetToTabExpanded(diffStr, detailedDiffStart + detailedDiffLength, - miningParam.tabWidth); - int expandedLength = expandedEnd - expandedStart; - if (expandedStart >= 0 && expandedLength > 0 && expandedStart + expandedLength <= txt.length()) { - StyleRange bgRange = new StyleRange(); - bgRange.start = expandedStart; - bgRange.length = expandedLength; - bgRange.background = miningParam.detailedDiffColor; - ranges.add(bgRange); - } + if (detailedDiffStr.trim().length() == 0) { + continue; } - return ranges; - } - - private void setTextEditorActionsActivated(boolean state) { - IEditorPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage() - .getActiveEditor(); - if (part instanceof MultiPageEditorPart multiPageEditorPart) { - Object page = multiPageEditorPart.getSelectedPage(); - if (page instanceof IEditorPart editorPart) { - part = editorPart; + if (detailedDiffStart + detailedDiffLength >= trimmedDiffStr.length()) { + int delta = diffStr.length() - trimmedDiffStr.length(); + if (detailedDiffLength <= delta) { + continue; } + detailedDiffLength -= delta; } - if (!(part instanceof AbstractTextEditor) || part.getSite().getWorkbenchWindow().isClosing()) { - return; - } - if (UnifiedDiffManager.isViewerInPart(part, viewer)) { - try { - Method method = AbstractTextEditor.class.getDeclaredMethod("setActionActivation", //$NON-NLS-1$ - boolean.class); - method.setAccessible(true); - method.invoke(part, Boolean.valueOf(state)); - } catch (IllegalArgumentException | ReflectiveOperationException ex) { - error(ex); - } + int expandedStart = mapOffsetToTabExpanded(diffStr, detailedDiffStart, tabWidth); + int expandedEnd = mapOffsetToTabExpanded(diffStr, detailedDiffStart + detailedDiffLength, tabWidth); + int expandedLength = expandedEnd - expandedStart; + if (expandedStart >= 0 && expandedLength > 0 && expandedStart + expandedLength <= txt.length()) { + StyleRange bgRange = new StyleRange(); + bgRange.start = expandedStart; + bgRange.length = expandedLength; + bgRange.background = detailedDiffColor; + ranges.add(bgRange); } } - } - - @Override - public String getLabel() { - return this.unifiedDiffLabel; - } - - public UnifiedDiff getUnifiedDiff() { - return this.diff; + return ranges; } /** @@ -847,22 +951,22 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { } // foregrounds for (var f : foregrounds) { - if (y + f.y < 0) { + if (y + f.y() < 0) { continue; } - if (f.font == null) { + if (f.font() == null) { gc.setFont(cachedFont); - } else if (!f.font.isDisposed()) { - gc.setFont(f.font); + } else if (!f.font().isDisposed()) { + gc.setFont(f.font()); } else { cleanCachedData(); cachedFont = font; fontIsDisposed = true; break; } - gc.setBackground(f.background); - gc.setForeground(f.foreground); - gc.drawString(f.str, x + f.x, y + f.y, true); + gc.setBackground(f.background()); + gc.setForeground(f.foreground()); + gc.drawString(f.str(), x + f.x(), y + f.y(), true); } if (!fontIsDisposed) { lastRectangle = new Rectangle(x, y, lastRectangle.width, lastRectangle.height); @@ -1002,70 +1106,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { } foregrounds = new ArrayList<>(); gc.setFont(cachedFont); - int textWidgetLineHeight = textWidget.getLineHeight(); - int cx = x; - int cy = y; - for (StyleRange range : ranges) { - String sub = label.substring(range.start, range.start + range.length); - if (sub.trim().length() > 0) { - if (range.background != null) { - gc.setBackground(range.background); - } - if (range.foreground != null) { - gc.setForeground(range.foreground); - } - Font currentFont = gc.getFont(); - var rangeWithFont = transformFontStyleToFont(currentFont, range); - if (rangeWithFont.font != null) { - gc.setFont(rangeWithFont.font); - } - String[] lines = sub.split("\n"); //$NON-NLS-1$ - if (lines.length > 1) { - for (int i = 0; i < lines.length; i++) { - String line = lines[i].replace("\r", ""); //$NON-NLS-1$ //$NON-NLS-2$ - gc.drawString(line, cx, cy, true); - foregrounds.add(new ForegroundInfo(cx - x, cy - y, line, gc.getFont(), gc.getBackground(), - gc.getForeground())); - Point p = gc.stringExtent(line); - if (i < lines.length - 1) { - cy += textWidgetLineHeight + textWidget.getLineSpacing(); - cx = x; - } else { - if (sub.endsWith("\n")) { //$NON-NLS-1$ - cy += textWidgetLineHeight + textWidget.getLineSpacing(); - cx = x; - } else { - cx += p.x; - } - } - } - } else { - gc.drawString(sub, cx, cy, true); - foregrounds.add(new ForegroundInfo(cx - x, cy - y, sub, gc.getFont(), gc.getBackground(), - gc.getForeground())); - Point p = gc.stringExtent(sub); - if (sub.endsWith("\n")) { //$NON-NLS-1$ - cy += textWidgetLineHeight + textWidget.getLineSpacing(); - cx = x; - } else { - cx += p.x; - } - } - gc.setFont(currentFont); - } else { - int lfCount = 0; - if (sub.contains("\n")) { //$NON-NLS-1$ - lfCount = sub.split("\n", -1).length - 1; //$NON-NLS-1$ - sub = sub.substring(sub.lastIndexOf("\n") + 1); //$NON-NLS-1$ - } - Point p = gc.stringExtent(sub); - if (lfCount > 0) { - cy += lfCount * (textWidgetLineHeight + textWidget.getLineSpacing()); - cx = x; - } - cx += p.x; - } - } + drawStyleRanges(gc, textWidget, ranges, label, styledFonts, x, y, foregrounds::add); return result; } @@ -1168,21 +1209,7 @@ private boolean isLastForCurrentOffset(List ranges, int i, int offse } private StyleRange transformFontStyleToFont(Font baseFont, StyleRange styleRange) { - // as per the StyleRange contract, only consider fontStyle if font is not - // already set - if (styleRange.font == null && styleRange.fontStyle > 0) { - StyleRange newRange = (StyleRange) styleRange.clone(); - newRange.font = styledFonts.computeIfAbsent(baseFont, f -> new HashMap<>()) - .computeIfAbsent(Integer.valueOf(styleRange.fontStyle), s -> { - FontData[] fontDatas = baseFont.getFontData(); - for (FontData fontData : fontDatas) { - fontData.setStyle(styleRange.fontStyle); - } - return new Font(baseFont.getDevice(), fontDatas); - }); - return newRange; - } - return styleRange; + return UnifiedDiffCodeMiningProvider.transformFontStyleToFont(styledFonts, baseFont, styleRange); } private int getOffsetAtLine(String str, int off) { @@ -1216,6 +1243,74 @@ private int getYForLine(int line, int y, GC gc, StyledText textWidget) { } } + static void openOverlay(StyledText overlay, ITextViewer viewer) { + overlay.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + overlay.dispose(); + setTextEditorActionsActivated(viewer, true); + } + }); + overlay.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.keyCode == SWT.ESC) { + overlay.dispose(); + setTextEditorActionsActivated(viewer, true); + } + e.doit = false; + } + }); + setTextEditorActionsActivated(viewer, false); + } + + static void setTextEditorActionsActivated(ITextViewer viewer, boolean state) { + IEditorPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor(); + if (part instanceof MultiPageEditorPart multiPageEditorPart) { + Object page = multiPageEditorPart.getSelectedPage(); + if (page instanceof IEditorPart editorPart) { + part = editorPart; + } + } + if (!(part instanceof AbstractTextEditor) || part.getSite().getWorkbenchWindow().isClosing()) { + return; + } + if (UnifiedDiffManager.isViewerInPart(part, viewer)) { + try { + Method method = AbstractTextEditor.class.getDeclaredMethod("setActionActivation", //$NON-NLS-1$ + boolean.class); + method.setAccessible(true); + method.invoke(part, Boolean.valueOf(state)); + } catch (IllegalArgumentException | ReflectiveOperationException ex) { + error(ex); + } + } + } + + /** + * Returns a {@link StyleRange} whose {@code font} carries the range's font + * style. Fonts are cached in the caller-owned {@code styledFonts} map so the + * cache lifetime stays tied to the owning mining instance. + */ + static StyleRange transformFontStyleToFont(Map> styledFonts, Font baseFont, + StyleRange styleRange) { + // as per the StyleRange contract, only consider fontStyle if font is not + // already set + if (styleRange.font == null && styleRange.fontStyle > 0) { + StyleRange newRange = (StyleRange) styleRange.clone(); + newRange.font = styledFonts.computeIfAbsent(baseFont, f -> new HashMap<>()) + .computeIfAbsent(Integer.valueOf(styleRange.fontStyle), s -> { + FontData[] fontDatas = baseFont.getFontData(); + for (FontData fontData : fontDatas) { + fontData.setStyle(styleRange.fontStyle); + } + return new Font(baseFont.getDevice(), fontDatas); + }); + return newRange; + } + return styleRange; + } + // from inner class ColorPalette in TextMergeViewer static RGB interpolate(RGB fg, RGB bg, double scale) { if (fg != null && bg != null) { 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 6505ec9b0db..7fb7b03adb4 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 @@ -33,6 +33,7 @@ import org.eclipse.compare.unifieddiff.UnifiedDiffMode; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.FoldedRegionCodeMining; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffFooterCodeMining; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffLineHeaderCodeMining; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; @@ -44,11 +45,19 @@ import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.TextAttribute; import org.eclipse.jface.text.codemining.ICodeMining; import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.presentation.IPresentationReconciler; +import org.eclipse.jface.text.presentation.PresentationReconciler; +import org.eclipse.jface.text.rules.DefaultDamagerRepairer; +import org.eclipse.jface.text.rules.RuleBasedScanner; +import org.eclipse.jface.text.rules.Token; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationModel; import org.eclipse.jface.text.source.AnnotationPainter; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.inlined.AbstractInlinedAnnotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotation; @@ -58,6 +67,7 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.text.undo.DocumentUndoManagerRegistry; +import org.eclipse.swt.graphics.GC; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -90,6 +100,7 @@ public void setUp() { document = numberedLines(60); model = new AnnotationModel(); viewer = new ProjectionViewer(shell, null, null, false, SWT.V_SCROLL); + viewer.configure(syntaxColoringConfiguration()); viewer.setDocument(document, model); viewer.enableProjection(); DocumentUndoManagerRegistry.connect(document); @@ -362,8 +373,90 @@ public void testReopeningNextToAnAsynchronousProviderKeepsEveryMining() throws E waitForAttachedMinings(diffs, allRegions().size()); } + /** + * When the document does not end with a newline, the diff at the end cannot + * anchor a line-header mining (there is no following line to indent). A footer + * mining must be created instead. + */ + @Test + public void testFooterMiningIsCreatedWhenDocumentHasNoTrailingNewline() throws Exception { + switchToDocument(new Document("line 0\nline 1 changed")); + + IStatus status = UnifiedDiffManager.open(viewer, document, model, null, "line 0\nline 1\n", MODE, null, null, + null, true, CONTEXT_LINES); + assertTrue(status.isOK(), "open() should succeed: " + status); + + List minings = provide(); + List footers = new ArrayList<>(); + for (ICodeMining mining : minings) { + if (mining instanceof UnifiedDiffFooterCodeMining footer) { + footers.add(footer); + } + } + assertThat(footers).as("a footer mining is used for the diff at the end of a document without trailing newline") + .isNotEmpty(); + } + + /** + * The footer mining draws through the same {@code drawStyleRanges} path as the + * line-header mining. With a presentation reconciler configured, + * {@code computeStyleRanges} yields ranges, so {@code draw()} runs the shared + * syntax-coloring path rather than the no-reconciler fallback; it must complete + * without throwing and set {@code lastRectangle}. + */ + @Test + public void testFooterMiningStyleRangesUseTheSameLogicAsTheHeader() throws Exception { + switchToDocument(new Document("line 0\nline 1 changed")); + + IStatus status = UnifiedDiffManager.open(viewer, document, model, null, "line 0\nline 1\n", MODE, null, null, + null, true, CONTEXT_LINES); + assertTrue(status.isOK(), "open() should succeed: " + status); + + List minings = provide(); + UnifiedDiffFooterCodeMining footer = null; + for (ICodeMining mining : minings) { + if (mining instanceof UnifiedDiffFooterCodeMining f) { + footer = f; + } + } + assertNotNull(footer, "a footer mining must be present"); + assertThat(footer.getLastRectangle()).as("lastRectangle starts null before the first draw").isNull(); + + GC gc = new GC(shell); + try { + footer.draw(gc, viewer.getTextWidget(), null, 0, 0); + } finally { + gc.dispose(); + } + + assertThat(footer.getLastRectangle()) + .as("draw() must set lastRectangle — it stayed null, so draw() bailed out before rendering") + .isNotNull(); + } + // ------------------------------------------------------------------ helpers + /** + * A configuration whose presentation reconciler colors the whole document with + * a single token, so {@code computeStyleRanges} returns ranges and the minings + * exercise their real syntax-coloring path instead of the no-reconciler + * fallback. + */ + private static SourceViewerConfiguration syntaxColoringConfiguration() { + return new SourceViewerConfiguration() { + @Override + public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) { + PresentationReconciler reconciler = new PresentationReconciler(); + RuleBasedScanner scanner = new RuleBasedScanner(); + scanner.setDefaultReturnToken(new Token(new TextAttribute(null))); + DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner); + reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE); + reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE); + return reconciler; + } + }; + } + /** A provider that answers only when the test lets it, like a slow editor. */ private final class PendingProvider implements ICodeMiningProvider { @@ -422,6 +515,8 @@ private void assertMinings(List minings, List diffs) { for (ICodeMining mining : minings) { if (mining instanceof UnifiedDiffLineHeaderCodeMining overlay) { shown.add(overlay.getUnifiedDiff()); + } else if (mining instanceof UnifiedDiffFooterCodeMining footer) { + shown.add(footer.getUnifiedDiff()); } else if (mining instanceof FoldedRegionCodeMining expander) { expanders.add(Integer.valueOf(expander.getPosition().getOffset())); } else { @@ -474,6 +569,8 @@ private void waitForAttachedMinings(List diffs, int regions) { for (ICodeMining mining : attachedMinings()) { if (mining instanceof UnifiedDiffLineHeaderCodeMining overlay) { shown.add(overlay.getUnifiedDiff()); + } else if (mining instanceof UnifiedDiffFooterCodeMining footer) { + shown.add(footer.getUnifiedDiff()); } else if (mining instanceof FoldedRegionCodeMining) { expanders++; } @@ -571,4 +668,17 @@ private static IDocument numberedLines(int count) { } return new Document(content.toString()); } + + /** + * Replaces the viewer's document mid-test. Disconnects the undo manager from + * the old document, connects it to the new one, and re-wires the viewer. + */ + private void switchToDocument(IDocument newDocument) { + DocumentUndoManagerRegistry.disconnect(document); + document = newDocument; + model = new AnnotationModel(); + viewer.setDocument(document, model); + DocumentUndoManagerRegistry.connect(document); + installCodeMinings(provider); + } }