From cf77eed6111cb43dcbacd699b0a83ee40caa2823 Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Tue, 29 Sep 2026 18:52:17 +0200 Subject: [PATCH] Allow overriding texts and images of unified diff toolbar actions The unified diff toolbars label their built-in actions with generic wording such as "Apply" or "Revert". That wording assumes the diff is shown against another revision of the same file, and it becomes misleading when a client shows a diff between two unrelated sources, because it does not say what taking over a change actually does. Callers can now override the text and the image of each built-in action through the builder, keyed by the new ToolbarAction enum. Overriding an image with null removes it, so the action shows its text instead. Text and image overrides are independent, and an absent entry keeps the default, so a caller only adapts the actions it cares about. --- .../compare/unifieddiff/UnifiedDiff.java | 72 +++++++- .../internal/ToolbarActionPresentations.java | 47 ++++++ .../internal/UnifiedDiffManager.java | 83 +++++++-- .../team/tests/ui/UnifiedDiffManagerTest.java | 157 ++++++++++++++++++ 4 files changed, 341 insertions(+), 18 deletions(-) create mode 100644 team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/ToolbarActionPresentations.java diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/UnifiedDiff.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/UnifiedDiff.java index b28d53110d1..e874940c42e 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/UnifiedDiff.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/UnifiedDiff.java @@ -13,16 +13,20 @@ *******************************************************************************/ package org.eclipse.compare.unifieddiff; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import org.eclipse.compare.contentmergeviewer.IIgnoreWhitespaceContributor; import org.eclipse.compare.contentmergeviewer.ITokenComparator; +import org.eclipse.compare.unifieddiff.internal.ToolbarActionPresentations; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.IDocument; import org.eclipse.ui.texteditor.ITextEditor; @@ -51,6 +55,41 @@ public static Builder create(ITextEditor editor, String source, UnifiedDiffMode return new Builder(editor, source, mode); } + /** + * Identifies a built-in action of a unified diff toolbar, so that its text or + * image can be replaced. Which actions are shown depends on the + * {@link UnifiedDiffMode}; unused constants are simply ignored. + * + * @see Builder#toolbarActionText(ToolbarAction, String) + * @see Builder#toolbarActionImage(ToolbarAction, ImageDescriptor) + */ + public enum ToolbarAction { + /** Applies every diff, in the toolbar shown for all diffs. */ + ACCEPT_ALL, + /** Discards every diff, in the toolbar shown for all diffs. */ + HIDE_ALL, + /** Reverts every diff, in the toolbar shown for all diffs. */ + REVERT_ALL, + /** Keeps every diff, in the toolbar shown for all diffs. */ + KEEP_ALL, + /** Undoes every diff, in the toolbar shown for all diffs. */ + UNDO_ALL, + /** Reveals the previous diff, in the toolbar shown for all diffs. */ + PREVIOUS, + /** Reveals the next diff, in the toolbar shown for all diffs. */ + NEXT, + /** Applies the hovered diff, in the toolbar shown for a single diff. */ + ACCEPT, + /** Discards the hovered diff, in the toolbar shown for a single diff. */ + HIDE, + /** Reverts the hovered diff, in the toolbar shown for a single diff. */ + REVERT, + /** Keeps the hovered diff, in the toolbar shown for a single diff. */ + KEEP, + /** Undoes the hovered diff, in the toolbar shown for a single diff. */ + UNDO + } + public static final class Builder { // Required parameters private final ITextEditor editor; @@ -63,6 +102,8 @@ public static final class Builder { private TokenComparatorFactory tokenComparatorFactory; private IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory; private int foldContextLines = -1; + private final Map toolbarActionTexts = new EnumMap<>(ToolbarAction.class); + private final Map toolbarActionImages = new EnumMap<>(ToolbarAction.class); private Builder(ITextEditor editor, String source, UnifiedDiffMode mode) { this.editor = Objects.requireNonNull(editor, "Editor cannot be null"); //$NON-NLS-1$ @@ -90,6 +131,34 @@ public Builder ignoreWhiteSpace(boolean value) { return this; } + /** + * Overrides the text, including the tooltip text, for an action in a unified + * diff toolbar. + * + * @param action the action whose text is overridden + * @param text the replacement text + * @return this builder + */ + public Builder toolbarActionText(ToolbarAction action, String text) { + toolbarActionTexts.put(Objects.requireNonNull(action, "Action cannot be null"), //$NON-NLS-1$ + Objects.requireNonNull(text, "Action text cannot be null")); //$NON-NLS-1$ + return this; + } + + /** + * Overrides the image for an action in a unified diff toolbar. Passing + * null removes the action's default image and displays its text + * instead. + * + * @param action the action whose image is overridden + * @param image the replacement image, or null to show text + * @return this builder + */ + public Builder toolbarActionImage(ToolbarAction action, ImageDescriptor image) { + toolbarActionImages.put(Objects.requireNonNull(action, "Action cannot be null"), image); //$NON-NLS-1$ + return this; + } + /** * Collapses unchanged regions between diffs, keeping the given number of * context lines (at least one) around each change. A negative value disables @@ -106,7 +175,8 @@ public Builder foldUnchanged(int contextLines) { public IStatus open() { return UnifiedDiffManager.open(editor, source, mode, additionalActions, tokenComparatorFactory, - ignoreWhitespaceContributorFactory, ignoreWhiteSpace, foldContextLines); + ignoreWhitespaceContributorFactory, ignoreWhiteSpace, foldContextLines, + new ToolbarActionPresentations(toolbarActionTexts, toolbarActionImages)); } } } diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/ToolbarActionPresentations.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/ToolbarActionPresentations.java new file mode 100644 index 00000000000..cc0c37da4e5 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/ToolbarActionPresentations.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.unifieddiff.internal; + +import java.util.EnumMap; +import java.util.Map; + +import org.eclipse.compare.unifieddiff.UnifiedDiff.ToolbarAction; +import org.eclipse.jface.resource.ImageDescriptor; + +/** + * The texts and images a caller supplied for the built-in toolbar actions. + * Absent entries fall back to the default of the action; an entry mapped to + * null is an override to "no image". + */ +public final class ToolbarActionPresentations { + + /** No overrides, so every action keeps its default text and image. */ + public static final ToolbarActionPresentations NONE = new ToolbarActionPresentations(Map.of(), Map.of()); + + private final Map texts = new EnumMap<>(ToolbarAction.class); + private final Map images = new EnumMap<>(ToolbarAction.class); + + public ToolbarActionPresentations(Map texts, Map images) { + this.texts.putAll(texts); + this.images.putAll(images); + } + + String text(ToolbarAction action, String defaultText) { + return texts.getOrDefault(action, defaultText); + } + + ImageDescriptor image(ToolbarAction action, ImageDescriptor defaultImage) { + return images.containsKey(action) ? images.get(action) : defaultImage; + } +} 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..b68becf9ab6 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 @@ -37,6 +37,7 @@ import org.eclipse.compare.rangedifferencer.RangeDifferencer; import org.eclipse.compare.unifieddiff.UnifiedDiff.IgnoreWhitespaceContributorFactory; import org.eclipse.compare.unifieddiff.UnifiedDiff.TokenComparatorFactory; +import org.eclipse.compare.unifieddiff.UnifiedDiff.ToolbarAction; import org.eclipse.compare.unifieddiff.UnifiedDiffMode; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffLineHeaderCodeMining; import org.eclipse.core.resources.IFile; @@ -123,9 +124,11 @@ public class UnifiedDiffManager { private static final String DETAILED_DELETION_ANNO_TYPE = "org.eclipse.compare.unifieddiff.internal.detailedDeletion"; //$NON-NLS-1$ private static final String TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY = "TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY"; //$NON-NLS-1$ private static final String TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY = "TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY"; //$NON-NLS-1$ + private static final String TOOLBAR_ACTION_PRESENTATIONS_KEY = "TOOLBAR_ACTION_PRESENTATIONS_KEY"; //$NON-NLS-1$ private static final Map> diffsByViewer = new HashMap<>(); /** + * The user canceled the computation. The text editor is already open, so the * caller must not fall back to the classic compare editor. Compared by identity, * because a status code would clash with {@link Status#CANCEL_STATUS}, which @@ -142,6 +145,16 @@ public static List get(ITextViewer viewer) { return diffsByViewer.get(viewer); } + private static void setToolbarActionPresentations(ITextViewer viewer, ToolbarActionPresentations presentations) { + viewer.getTextWidget().setData(TOOLBAR_ACTION_PRESENTATIONS_KEY, presentations); + } + + private static ToolbarActionPresentations toolbarActionPresentations(ITextViewer viewer) { + Object presentations = viewer.getTextWidget().getData(TOOLBAR_ACTION_PRESENTATIONS_KEY); + return presentations instanceof ToolbarActionPresentations result ? result : ToolbarActionPresentations.NONE; + } + + /** * Returns the annotation model the diffs of the given editor live in, or * null when it has none. The document provider of an editor on @@ -174,6 +187,14 @@ public static IStatus open(ITextEditor editor, String source, UnifiedDiffMode mo TokenComparatorFactory tokenComparatorFactory, IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory, boolean ignoreWhiteSpace, int foldContextLines) { + return open(editor, source, mode, additionalActions, tokenComparatorFactory, ignoreWhitespaceContributorFactory, + ignoreWhiteSpace, foldContextLines, ToolbarActionPresentations.NONE); + } + + public static IStatus open(ITextEditor editor, String source, UnifiedDiffMode mode, List additionalActions, + TokenComparatorFactory tokenComparatorFactory, + IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory, boolean ignoreWhiteSpace, + int foldContextLines, ToolbarActionPresentations toolbarActionPresentations) { ITextViewer viewer = editor.getAdapter(ITextViewer.class); IDocument leftDocument = editor.getDocumentProvider().getDocument(editor.getEditorInput()); IAnnotationModel editorModel = annotationModelOf(editor); @@ -183,7 +204,7 @@ public static IStatus open(ITextEditor editor, String source, UnifiedDiffMode mo } IFile file = editor.getEditorInput().getAdapter(IFile.class); return open(viewer, leftDocument, model, file, source, mode, additionalActions, tokenComparatorFactory, - ignoreWhitespaceContributorFactory, ignoreWhiteSpace, foldContextLines); + ignoreWhitespaceContributorFactory, ignoreWhiteSpace, foldContextLines, toolbarActionPresentations); } /** @@ -196,6 +217,17 @@ public static IStatus open(ITextViewer viewer, IDocument leftDocument, IAnnotati TokenComparatorFactory tokenComparatorFactory, IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory, boolean ignoreWhiteSpace, int foldContextLines) { + return open(viewer, leftDocument, model, file, source, mode, additionalActions, tokenComparatorFactory, + ignoreWhitespaceContributorFactory, ignoreWhiteSpace, foldContextLines, + ToolbarActionPresentations.NONE); + } + + public static IStatus open(ITextViewer viewer, IDocument leftDocument, IAnnotationModel model, IFile file, + String source, UnifiedDiffMode mode, List additionalActions, + TokenComparatorFactory tokenComparatorFactory, + IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory, boolean ignoreWhiteSpace, + int foldContextLines, ToolbarActionPresentations toolbarActionPresentations) { + if (viewer instanceof ProjectionViewer pv) { pv.doOperation(ProjectionViewer.EXPAND_ALL); removeFoldAnnotations(pv); @@ -279,12 +311,14 @@ public static IStatus open(ITextViewer viewer, IDocument leftDocument, IAnnotati } UnifiedDiffManager.put(viewer, unifiedDiffs); + setToolbarActionPresentations(viewer, toolbarActionPresentations); addPaintListener(viewer, model, mode); addMouseMoveListener(viewer, model); if (viewer instanceof ISourceViewerExtension5 ext) { ext.updateCodeMinings(); } - drawToolBarForAllDiffs(viewer, model, additionalActions, mode); + drawToolBarForAllDiffs(viewer, model, additionalActions, mode, toolbarActionPresentations); + addUndoListener(viewer, leftDocument, model); addAnnoModelChangeListener(viewer, model); @@ -969,33 +1003,41 @@ public UnifiedDiff getUnifiedDiff() { } private static void drawToolBarForAllDiffs(ITextViewer tv, IAnnotationModel model, List additionalActions, - UnifiedDiffMode mode) { + UnifiedDiffMode mode, ToolbarActionPresentations presentations) { StyledText tw = tv.getTextWidget(); var tm = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT); if (UnifiedDiffMode.OVERLAY_MODE.equals(mode) || UnifiedDiffMode.OVERLAY_READ_ONLY_MODE.equals(mode)) { List diffs = get(tv); if (!isReadOnly(diffs)) { var acceptAll = new AcceptAllRunnable(tv, model); - addToolbarAction(tm, acceptAll.getLabel(), AcceptAllRunnable.getImageDescriptor(), acceptAll); + addToolbarAction(tm, presentations.text(ToolbarAction.ACCEPT_ALL, acceptAll.getLabel()), + presentations.image(ToolbarAction.ACCEPT_ALL, AcceptAllRunnable.getImageDescriptor()), acceptAll); } var hideAll = new HideAllDiffsRunnable(tv, model); - addToolbarAction(tm, hideAll.getLabel(), hideAll.getImageDescriptor(), hideAll); + addToolbarAction(tm, presentations.text(ToolbarAction.HIDE_ALL, hideAll.getLabel()), + presentations.image(ToolbarAction.HIDE_ALL, hideAll.getImageDescriptor()), hideAll); } else if (UnifiedDiffMode.REVERT_MODE.equals(mode)) { var revertAll = new AcceptAllRunnable(tv, model); - addToolbarAction(tm, CompareMessages.UnifiedDiff_revert, AcceptAllRunnable.getUndoImageDescriptor(), revertAll); + addToolbarAction(tm, presentations.text(ToolbarAction.REVERT_ALL, CompareMessages.UnifiedDiff_revert), + presentations.image(ToolbarAction.REVERT_ALL, AcceptAllRunnable.getUndoImageDescriptor()), revertAll); var hideAll = new HideAllDiffsRunnable(tv, model); - addToolbarAction(tm, hideAll.getLabel(), hideAll.getImageDescriptor(), hideAll); + addToolbarAction(tm, presentations.text(ToolbarAction.HIDE_ALL, hideAll.getLabel()), + presentations.image(ToolbarAction.HIDE_ALL, hideAll.getImageDescriptor()), hideAll); } else { var keepAll = new KeepAllRunnable(tv, model); - addToolbarAction(tm, keepAll.getLabel(), null, keepAll); + addToolbarAction(tm, presentations.text(ToolbarAction.KEEP_ALL, keepAll.getLabel()), + presentations.image(ToolbarAction.KEEP_ALL, null), keepAll); var undoAll = new UndoAllRunnable(tv, model); - addToolbarAction(tm, undoAll.getLabel(), null, undoAll); + addToolbarAction(tm, presentations.text(ToolbarAction.UNDO_ALL, undoAll.getLabel()), + presentations.image(ToolbarAction.UNDO_ALL, null), undoAll); } var previous = new PreviousRunnable(tv, model, tm); - addToolbarAction(tm, previous.getLabel(), previous.getImageDescriptor(), previous); + addToolbarAction(tm, presentations.text(ToolbarAction.PREVIOUS, previous.getLabel()), + presentations.image(ToolbarAction.PREVIOUS, previous.getImageDescriptor()), previous); var next = new NextRunnable(tv, model, tm); - addToolbarAction(tm, next.getLabel(), next.getImageDescriptor(), next); + addToolbarAction(tm, presentations.text(ToolbarAction.NEXT, next.getLabel()), + presentations.image(ToolbarAction.NEXT, next.getImageDescriptor()), next); if (additionalActions != null) { for (var additionalAction : additionalActions) { addToolbarAction(tm, additionalAction); @@ -1206,22 +1248,29 @@ private static void dismissCurrentDiff(ITextViewer tv, IAnnotationModel model) { private static void drawToolbarForOneDiff(ITextViewer tv, IAnnotationModel model) { List diffs = get(tv); StyledText tw = tv.getTextWidget(); + ToolbarActionPresentations presentations = toolbarActionPresentations(tv); var tm = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT); if (isOverlay(diffs)) { if (!isReadOnly(diffs)) { - addToolbarAction(tm, CompareMessages.UnifiedDiff_accept, AcceptAllRunnable.getImageDescriptor(), + addToolbarAction(tm, presentations.text(ToolbarAction.ACCEPT, CompareMessages.UnifiedDiff_accept), + presentations.image(ToolbarAction.ACCEPT, AcceptAllRunnable.getImageDescriptor()), () -> applyDiffRightStr(tv, model)); } - addToolbarAction(tm, CompareMessages.UnifiedDiff_hideDiff, HideAllDiffsRunnable.getHideDiffImageDescriptor(), + addToolbarAction(tm, presentations.text(ToolbarAction.HIDE, CompareMessages.UnifiedDiff_hideDiff), + presentations.image(ToolbarAction.HIDE, HideAllDiffsRunnable.getHideDiffImageDescriptor()), () -> dismissCurrentDiff(tv, model)); } else if (isRevert(diffs)) { - addToolbarAction(tm, CompareMessages.UnifiedDiff_revert, AcceptAllRunnable.getUndoImageDescriptor(), + addToolbarAction(tm, presentations.text(ToolbarAction.REVERT, CompareMessages.UnifiedDiff_revert), + presentations.image(ToolbarAction.REVERT, AcceptAllRunnable.getUndoImageDescriptor()), () -> applyDiffRightStr(tv, model)); - addToolbarAction(tm, CompareMessages.UnifiedDiff_hideDiff, HideAllDiffsRunnable.getHideDiffImageDescriptor(), + addToolbarAction(tm, presentations.text(ToolbarAction.HIDE, CompareMessages.UnifiedDiff_hideDiff), + presentations.image(ToolbarAction.HIDE, HideAllDiffsRunnable.getHideDiffImageDescriptor()), () -> dismissCurrentDiff(tv, model)); } else { - addToolbarAction(tm, CompareMessages.UnifiedDiff_keep, null, () -> dismissCurrentDiff(tv, model)); - addToolbarAction(tm, CompareMessages.UnifiedDiff_undo, null, () -> { + addToolbarAction(tm, presentations.text(ToolbarAction.KEEP, CompareMessages.UnifiedDiff_keep), + presentations.image(ToolbarAction.KEEP, null), () -> dismissCurrentDiff(tv, model)); + addToolbarAction(tm, presentations.text(ToolbarAction.UNDO, CompareMessages.UnifiedDiff_undo), + presentations.image(ToolbarAction.UNDO, null), () -> { var fToolbarShellForOneDiff = getToolbarCompositeForOneDiff(tv.getTextWidget()); if (fToolbarShellForOneDiff == null) { return; diff --git a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java index fc149d13983..d5a2e1eae6f 100644 --- a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java +++ b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java @@ -22,15 +22,18 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.function.Function; import org.eclipse.compare.contentmergeviewer.ITokenComparator; import org.eclipse.compare.rangedifferencer.IRangeComparator; import org.eclipse.compare.unifieddiff.UnifiedDiff; +import org.eclipse.compare.unifieddiff.UnifiedDiff.ToolbarAction; import org.eclipse.compare.unifieddiff.UnifiedDiffMode; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager; import org.eclipse.core.resources.IFile; @@ -55,6 +58,8 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.IDE; @@ -77,6 +82,7 @@ public class UnifiedDiffManagerTest { // StyledText#getData(...) keys used by UnifiedDiffManager to remember the // per-diff toolbar and the annotation it was opened for. private static final String TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY = "TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY"; + private static final String TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY = "TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY"; private static final String CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY = "CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY"; private static final String LEFT = """ @@ -473,6 +479,131 @@ public void testMouseMoveOpensTheToolbarForTheHoveredDiff() throws BadLocationEx "the diff under the mouse cursor must be the one whose toolbar is shown"); } + @Test + public void testToolbarActionPresentationCanOverrideTextAndRemoveImage() throws BadLocationException { + setEditorContent(LEFT); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(ToolbarAction.ACCEPT_ALL, "Apply all changes") + .toolbarActionImage(ToolbarAction.ACCEPT_ALL, null) + .toolbarActionText(ToolbarAction.ACCEPT, "Apply change") + .toolbarActionImage(ToolbarAction.ACCEPT, null).open().isOK()); + + StyledText tw = viewer().getTextWidget(); + Composite allDiffsToolbar = (Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY); + assertNotNull(allDiffsToolbar, "the all-diffs toolbar must be shown"); + assertTextOnlyToolbarItem(toolbar(allDiffsToolbar), "Apply all changes"); + + Position pos = annotationModel().getPosition(annotations(annotationModel(), DELETION_ANNO_TYPE).get(0)); + fireMouseMove(tw, tw.getLinePixel(widgetLineOfModelOffset(viewer(), pos.offset)) + 2); + processEvents(); + + Composite oneDiffToolbar = (Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY); + assertNotNull(oneDiffToolbar, "the per-diff toolbar must be shown"); + assertTextOnlyToolbarItem(toolbar(oneDiffToolbar), "Apply change"); + } + + @Test + public void testToolbarActionPresentationInRevertMode() throws BadLocationException { + setEditorContent(LEFT); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.REVERT_MODE) + .toolbarActionText(ToolbarAction.REVERT_ALL, "Revert everything") + .toolbarActionImage(ToolbarAction.REVERT_ALL, null) + .toolbarActionText(ToolbarAction.REVERT, "Revert this") + .toolbarActionImage(ToolbarAction.REVERT, null).open().isOK()); + + StyledText tw = viewer().getTextWidget(); + assertTextOnlyToolbarItem(toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY)), + "Revert everything"); + + Position pos = annotationModel().getPosition(annotations(annotationModel(), ADDITION_ANNO_TYPE).get(0)); + fireMouseMove(tw, tw.getLinePixel(widgetLineOfModelOffset(viewer(), pos.offset)) + 2); + processEvents(); + + assertTextOnlyToolbarItem(toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY)), "Revert this"); + } + + @Test + public void testToolbarActionPresentationInKeepUndoMode() throws BadLocationException { + setEditorContent(LEFT); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.REPLACE_MODE) + .toolbarActionText(ToolbarAction.KEEP_ALL, "Keep everything") + .toolbarActionText(ToolbarAction.KEEP, "Keep this").open().isOK()); + + StyledText tw = viewer().getTextWidget(); + assertTextOnlyToolbarItem(toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY)), + "Keep everything"); + + Position pos = annotationModel().getPosition(annotations(annotationModel(), ADDITION_ANNO_TYPE).get(0)); + fireMouseMove(tw, tw.getLinePixel(widgetLineOfModelOffset(viewer(), pos.offset)) + 2); + processEvents(); + + assertTextOnlyToolbarItem(toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ONE_DIFF_KEY)), "Keep this"); + } + + /** + * Overriding text without overriding the image must leave the default image in + * place (the two overrides are independent). + */ + @Test + public void testToolbarActionTextOnlyOverrideKeepsDefaultImage() throws BadLocationException { + setEditorContent(LEFT); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(ToolbarAction.ACCEPT_ALL, "My custom label").open().isOK()); + + StyledText tw = viewer().getTextWidget(); + ToolBar toolbar = toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY)); + // an action with an image shows its text only as the tooltip + ToolItem item = findToolbarItem(toolbar, ToolItem::getToolTipText, "My custom label"); + assertNotNull(item, "toolbar item with overridden tooltip must exist"); + assertNotNull(item.getImage(), "a text-only override must not remove the default image"); + } + + /** + * Opening a second diff on the same editor must replace the first toolbar + * presentations, not accumulate them. + */ + @Test + public void testReopeningReplacesToolbarPresentations() throws BadLocationException { + setEditorContent(LEFT); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(ToolbarAction.ACCEPT_ALL, "First label") + .toolbarActionImage(ToolbarAction.ACCEPT_ALL, null).open().isOK()); + + assertTrue(UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(ToolbarAction.ACCEPT_ALL, "Second label") + .toolbarActionImage(ToolbarAction.ACCEPT_ALL, null).open().isOK()); + + StyledText tw = viewer().getTextWidget(); + ToolBar toolbar = toolbar((Composite) tw.getData(TOOLBAR_COMPOSITE_FOR_ALL_DIFFS_KEY)); + assertNull(findToolbarItem(toolbar, ToolItem::getText, "First label"), + "first label must no longer appear after re-open"); + assertNotNull(findToolbarItem(toolbar, ToolItem::getText, "Second label"), + "second label must be shown after re-open"); + } + + @Test + public void testToolbarActionTextRejectsNullAction() { + assertThrows(NullPointerException.class, () -> UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(null, "some text")); + } + + @Test + public void testToolbarActionTextRejectsNullText() { + assertThrows(NullPointerException.class, () -> UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionText(ToolbarAction.ACCEPT_ALL, null)); + } + + @Test + public void testToolbarActionImageRejectsNullAction() { + assertThrows(NullPointerException.class, () -> UnifiedDiff.create(editor, RIGHT, UnifiedDiffMode.OVERLAY_MODE) + .toolbarActionImage(null, null)); + } + // ------------------------------------------------------------------ helpers private void assertReplaceModeYields(String left, String right) { @@ -537,6 +668,32 @@ private static void fireMouseMove(StyledText tw, int y) { tw.getTypedListeners(SWT.MouseMove, MouseMoveListener.class).forEach(listener -> listener.mouseMove(mouseEvent)); } + private static ToolBar toolbar(Composite composite) { + assertNotNull(composite, "the toolbar composite must be shown"); + for (var child : composite.getChildren()) { + if (child instanceof ToolBar toolbar) { + return toolbar; + } + } + throw new AssertionError("toolbar control not found"); + } + + private static ToolItem findToolbarItem(ToolBar toolbar, Function property, String expected) { + for (ToolItem item : toolbar.getItems()) { + if (expected.equals(property.apply(item))) { + return item; + } + } + return null; + } + + /** Asserts that an action without an image is shown with the given text. */ + private static void assertTextOnlyToolbarItem(ToolBar toolbar, String text) { + ToolItem item = findToolbarItem(toolbar, ToolItem::getText, text); + assertNotNull(item, "toolbar item not found: " + text); + assertNull(item.getImage(), "an action without an image must display its text"); + } + /** The widget line the given model (document) offset is displayed on. */ private static int widgetLineOfModelOffset(ITextViewer viewer, int modelOffset) { int widgetOffset = modelOffset;