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;