diff --git a/team/bundles/org.eclipse.compare/META-INF/MANIFEST.MF b/team/bundles/org.eclipse.compare/META-INF/MANIFEST.MF index 4fd4c9bb3fe..ac3bfeaad9a 100644 --- a/team/bundles/org.eclipse.compare/META-INF/MANIFEST.MF +++ b/team/bundles/org.eclipse.compare/META-INF/MANIFEST.MF @@ -12,7 +12,8 @@ Export-Package: org.eclipse.compare, org.eclipse.compare.internal.merge;x-internal:=true, org.eclipse.compare.internal.patch;x-friends:="org.eclipse.team.ui", org.eclipse.compare.patch, - org.eclipse.compare.structuremergeviewer + org.eclipse.compare.structuremergeviewer, + org.eclipse.compare.unifieddiff Require-Bundle: org.eclipse.ui;bundle-version="[3.206.0,4.0.0)", org.eclipse.core.resources;bundle-version="[3.4.0,4.0.0)", org.eclipse.jface.text;bundle-version="[3.8.0,4.0.0)", 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 f37564f5dc0..5ad6aa09a8f 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 @@ -2063,9 +2063,15 @@ protected void handleDispose(DisposeEvent event) { fBirdsEyeCanvas= null; fSummaryHeader= null; - fAncestorContributor.unsetDocument(fAncestor); - fLeftContributor.unsetDocument(fLeft); - fRightContributor.unsetDocument(fRight); + if (fAncestorContributor != null) { + fAncestorContributor.unsetDocument(fAncestor); + } + if (fLeftContributor != null) { + fLeftContributor.unsetDocument(fLeft); + } + if (fRightContributor != null) { + fRightContributor.unsetDocument(fRight); + } disconnect(fLeftContributor); disconnect(fRightContributor); @@ -5563,6 +5569,12 @@ public int getChangesCount() { } }; } + if (adapter == IDocumentMergerInput.class) { + if (fMerger == null) { + return null; + } + return (T) fMerger.getInput(); + } if (adapter == OutlineViewerCreator.class) { if (fOutlineViewerCreator == null) { fOutlineViewerCreator = new InternalOutlineViewerCreator(); diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ComparePreferencePage.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ComparePreferencePage.java index b4525dc9aca..372c8a70669 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ComparePreferencePage.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/ComparePreferencePage.java @@ -123,6 +123,7 @@ private String loadPreviewContentFromFile(String key) { public static final String ADDED_LINES_REGEX= PREFIX + "AddedLinesRegex"; //$NON-NLS-1$ public static final String REMOVED_LINES_REGEX= PREFIX + "RemovedLinesRegex"; //$NON-NLS-1$ public static final String SWAPPED = PREFIX + "Swapped"; //$NON-NLS-1$ + public static final String UNIFIED_DIFF = PREFIX + "UnitifedDiff"; //$NON-NLS-1$ private IPropertyChangeListener fPreferenceChangeListener; @@ -154,6 +155,7 @@ private String loadPreviewContentFromFile(String key) { new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, ICompareUIConstants.PREF_NAVIGATION_END_ACTION), new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, ICompareUIConstants.PREF_NAVIGATION_END_ACTION_LOCAL), new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, SWAPPED), + new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, UNIFIED_DIFF), }; private final List editors = new ArrayList<>(); private CTabItem fTextCompareTab; @@ -177,6 +179,7 @@ public static void initDefaults(IPreferenceStore store) { store.setDefault(ICompareUIConstants.PREF_NAVIGATION_END_ACTION, ICompareUIConstants.PREF_VALUE_PROMPT); store.setDefault(ICompareUIConstants.PREF_NAVIGATION_END_ACTION_LOCAL, ICompareUIConstants.PREF_VALUE_LOOP); store.setDefault(SWAPPED, false); + store.setDefault(UNIFIED_DIFF, false); } public ComparePreferencePage() { @@ -286,6 +289,7 @@ private Control createGeneralPage(Composite parent) { addCheckBox(composite, "ComparePreferencePage.structureCompare.label", OPEN_STRUCTURE_COMPARE, 0); //$NON-NLS-1$ addCheckBox(composite, "ComparePreferencePage.structureOutline.label", USE_OUTLINE_VIEW, 0); //$NON-NLS-1$ addCheckBox(composite, "ComparePreferencePage.ignoreWhitespace.label", IGNORE_WHITESPACE, 0); //$NON-NLS-1$ + addCheckBox(composite, "ComparePreferencePage.unifiedDiff.label", UNIFIED_DIFF, 0); //$NON-NLS-1$ // a spacer new Label(composite, SWT.NONE); diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java index c3279876656..5d82d9bc460 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java @@ -21,7 +21,9 @@ import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -43,13 +45,18 @@ import org.eclipse.compare.CompareConfiguration; import org.eclipse.compare.CompareEditorInput; import org.eclipse.compare.IResourceProvider; +import org.eclipse.compare.ISharedDocumentAdapter; import org.eclipse.compare.IStreamContentAccessor; import org.eclipse.compare.IStreamMerger; import org.eclipse.compare.ITypedElement; import org.eclipse.compare.internal.core.CompareSettings; +import org.eclipse.compare.internal.merge.DocumentMerger.IDocumentMergerInput; import org.eclipse.compare.structuremergeviewer.ICompareInput; import org.eclipse.compare.structuremergeviewer.IStructureCreator; +import org.eclipse.compare.structuremergeviewer.SharedDocumentAdapterWrapper; import org.eclipse.compare.structuremergeviewer.StructureDiffViewer; +import org.eclipse.compare.unifieddiff.UnifiedDiff; +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.Adapters; @@ -62,12 +69,14 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.content.IContentDescription; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; +import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.operation.IRunnableContext; import org.eclipse.jface.preference.IPreferenceStore; @@ -76,6 +85,7 @@ import org.eclipse.jface.viewers.Viewer; import org.eclipse.osgi.service.debug.DebugOptions; import org.eclipse.osgi.service.debug.DebugOptionsListener; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; @@ -91,8 +101,10 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; import org.eclipse.ui.model.IWorkbenchAdapter; import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.eclipse.ui.texteditor.ITextEditor; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; @@ -560,6 +572,10 @@ public void openCompareEditor(final CompareEditorInput input, CompareConfiguration configuration = input.getCompareConfiguration(); if (configuration != null) { IPreferenceStore ps= configuration.getPreferenceStore(); + boolean unifiedDiffEnabled = ps.getBoolean(ComparePreferencePage.UNIFIED_DIFF); + if (unifiedDiffEnabled && openUnifiedDiffInEditor(input, page, editor, activate)) { + return; + } if (ps != null) { configuration.setProperty( CompareConfiguration.USE_OUTLINE_VIEW, @@ -575,9 +591,155 @@ public void openCompareEditor(final CompareEditorInput input, } } - private void openEditorInBackground(final CompareEditorInput input, - final IWorkbenchPage page, final IReusableEditor editor, - final boolean activate) { + private boolean openUnifiedDiffInEditor(final CompareEditorInput input, final IWorkbenchPage page, + IReusableEditor editor, boolean activate) { + var unifiedDiffInput = canShowInUnifiedDiff(input); + if (unifiedDiffInput!=null) { + try { + IWorkbenchPage wpage = page != null ? page : getActivePage(); + IEditorPart rightEditor = wpage.openEditor(unifiedDiffInput.right, + getEditorId(unifiedDiffInput.right, unifiedDiffInput.rightElement)); + if (rightEditor instanceof ITextEditor rightTextEditor) { + Action openTwoWayCompare = new Action("Open in 2-way Compare Editor", SWT.PUSH) { + @Override + public void run() { + if (input.canRunAsJob()) { + openEditorInBackground(input, page, editor, activate); + } else { + if (compareResultOK(input, null)) { + internalOpenEditor(input, page, editor, activate); + } + } + } + }; + UnifiedDiff + .create(rightTextEditor, getSourceOf(unifiedDiffInput.leftAcessor()), + UnifiedDiffMode.OVERLAY_READ_ONLY_MODE) + .additionalActions(Arrays.asList(openTwoWayCompare)) + .ignoreWhitespaceContributorFactory( + t -> unifiedDiffInput.documentMergerInput != null + ? unifiedDiffInput.documentMergerInput.createIgnoreWhitespaceContributor(t) + : Optional.empty()) + .tokenComparatorFactory(t -> unifiedDiffInput.documentMergerInput != null + ? unifiedDiffInput.documentMergerInput.createTokenComparator(t) + : null) + .ignoreWhiteSpace(Utilities.getBoolean(input.getCompareConfiguration(), + CompareConfiguration.IGNORE_WHITESPACE, false)) + .open(); + } + return true; + } catch (PartInitException e) { + CompareUIPlugin.log(e); + } + } + return false; + } + + private String getSourceOf(IStreamContentAccessor right) { + try { + if (right == null) { + return ""; //$NON-NLS-1$ + } + String result = toString(right.getContents()); + return result; + } catch (CoreException | IOException e) { + CompareUIPlugin.log(e); + } + return null; + } + + private static String toString(InputStream inputStream) throws IOException { + if (inputStream == null) { + return ""; //$NON-NLS-1$ + } + try (inputStream) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private static String getEditorId(IEditorInput editorInput, ITypedElement element) { + String fileName = editorInput.getName(); + IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry(); + IContentType type = getContentType(element); + IEditorDescriptor descriptor = registry.getDefaultEditor(fileName, type); + IDE.overrideDefaultEditorAssociation(editorInput, type, descriptor); + String id; + if (descriptor == null || descriptor.isOpenExternal()) { + id = "org.eclipse.ui.DefaultTextEditor"; //$NON-NLS-1$ + } else { + id = descriptor.getId(); + } + return id; + } + + private static record LeftEditorInputAndRightStreamContentAccessor(IEditorInput left, ITypedElement leftElement, + IStreamContentAccessor leftAcessor, IEditorInput right, ITypedElement rightElement, + IStreamContentAccessor rightAcessor, IDocumentMergerInput documentMergerInput) { + } + + private LeftEditorInputAndRightStreamContentAccessor canShowInUnifiedDiff(CompareEditorInput input) { + IDocumentMergerInput documentMergerInput = null; + try { + input.run(new NullProgressMonitor()); + Object res = input.getCompareResult(); + if (!(res instanceof ICompareInput compareInput)) { + return null; + } + ITypedElement ancestor = compareInput.getAncestor(); + if (ancestor != null) { + return null; + } + // TODO (tm) do not support if one side is editable? + ITypedElement left = compareInput.getLeft(); + if (left == null) { + return null; + } + ISharedDocumentAdapter leftSda = SharedDocumentAdapterWrapper.getAdapter(left); + if (leftSda == null) { + return null; + } + IEditorInput leftEditorInput = leftSda.getDocumentKey(left); + if (leftEditorInput == null) { + return null; + } + if (!(left instanceof IStreamContentAccessor leftSa)) { + return null; + } + ITypedElement right = compareInput.getRight(); + if (right == null) { + return null; + } + ISharedDocumentAdapter rightSda = SharedDocumentAdapterWrapper.getAdapter(right); + if (rightSda == null) { + return null; + } + IEditorInput rightEditorInput = rightSda.getDocumentKey(right); + if (rightEditorInput == null) { + return null; + } + if (!(right instanceof IStreamContentAccessor rightSa)) { + return null; + } + var invisibleParent = new Shell(); + try { + invisibleParent.setVisible(false); + var viewer = input.findContentViewer(new NullViewer(getShell()), compareInput, invisibleParent); + if (viewer instanceof IAdaptable adaptable) { + documentMergerInput = adaptable.getAdapter(IDocumentMergerInput.class); + } + } finally { + invisibleParent.dispose(); + } + return new LeftEditorInputAndRightStreamContentAccessor(leftEditorInput, left, leftSa, rightEditorInput, + right, rightSa, documentMergerInput); + } catch (InvocationTargetException | InterruptedException e) { + CompareUIPlugin.log(e); + } + return null; + } + + private void openEditorInBackground(final CompareEditorInput input, final IWorkbenchPage page, + final IReusableEditor editor, final boolean activate) { internalOpenEditor(input, page, editor, activate); } diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/merge/DocumentMerger.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/merge/DocumentMerger.java index 63b2639bc1d..12d5f6d06d9 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/merge/DocumentMerger.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/merge/DocumentMerger.java @@ -1403,4 +1403,7 @@ private Diff findPrev(char contributor, List v, int start, int end, boolea return null; } + public IDocumentMergerInput getInput() { + return fInput; + } } 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 new file mode 100644 index 00000000000..e4c65b15921 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/UnifiedDiff.java @@ -0,0 +1,101 @@ +package org.eclipse.compare.unifieddiff; + +import java.util.List; +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.UnifiedDiffManager; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ui.texteditor.ITextEditor; + +public final class UnifiedDiff { + + private UnifiedDiff() { + } + + public static enum UnifiedDiffMode { + /** + * Diffs are directly applied in the editor. Users have the possibility to keep + * or undo the applied diffs. + */ + REPLACE_MODE, + /** + * The source in the editor is not modified. Diffs are shown as code mining and + * users have the possibility to apply or cancel individual diffs. + */ + OVERLAY_MODE, + /** + * The source in the editor is not modified. Diffs are shown as code mining and + * users cannot apply or dismiss the diffs (read-only mode). + */ + OVERLAY_READ_ONLY_MODE + } + + @FunctionalInterface + public static interface TokenComparatorFactory extends Function { + } + + @FunctionalInterface + public static interface IgnoreWhitespaceContributorFactory + extends Function> { + } + + /** + * Shows the unified diff in the given editor. + * @param editor the text editor where the diff will be shown + * @param source the source content to compare + * @param mode the mode in which the diff will be displayed + * @return a builder to configure and open the unified diff + */ + public static Builder create(ITextEditor editor, String source, UnifiedDiffMode mode) { + return new Builder(editor, source, mode); + } + + public static final class Builder { + // Required parameters + private final ITextEditor editor; + private final String source; + private final UnifiedDiffMode mode; + private boolean ignoreWhiteSpace = true; + + // Optional parameters + private List additionalActions; + private TokenComparatorFactory tokenComparatorFactory; + private IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory; + + private Builder(ITextEditor editor, String source, UnifiedDiffMode mode) { + this.editor = Objects.requireNonNull(editor, "Editor cannot be null"); //$NON-NLS-1$ + this.source = Objects.requireNonNull(source, "Source cannot be null"); //$NON-NLS-1$ + this.mode = Objects.requireNonNull(mode, "Mode cannot be null"); //$NON-NLS-1$ + } + + public Builder additionalActions(List actions) { + this.additionalActions = actions; + return this; + } + + public Builder ignoreWhitespaceContributorFactory(IgnoreWhitespaceContributorFactory factory) { + this.ignoreWhitespaceContributorFactory = factory; + return this; + } + + public Builder tokenComparatorFactory(TokenComparatorFactory factory) { + this.tokenComparatorFactory = factory; + return this; + } + + public Builder ignoreWhiteSpace(boolean value) { + ignoreWhiteSpace = value; + return this; + } + + public void open() { + UnifiedDiffManager.open(editor, source, mode, additionalActions, tokenComparatorFactory, + ignoreWhitespaceContributorFactory, ignoreWhiteSpace); + } + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/AcceptAllRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/AcceptAllRunnable.java new file mode 100644 index 00000000000..95d577b4a3d --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/AcceptAllRunnable.java @@ -0,0 +1,75 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.disposeUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.runAfterRepaintFinished; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiffAnnotation; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewerExtension5; +import org.eclipse.swt.custom.StyledText; + +public class AcceptAllRunnable implements Runnable { + private IAnnotationModel model; + private ITextViewer tv; + + public AcceptAllRunnable(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + public String getLabel() { + return "Accept All"; + } + + @Override + public void run() { + StyledText tw = tv.getTextWidget(); + List diffs1 = get(tv); + List positions = new ArrayList<>(); + List replaceStrings = new ArrayList<>(); + for (UnifiedDiff diff : diffs1) { + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + int unifiedDiffAdditionCount = 0; + for (var lanno : annos) { + if (lanno instanceof UnifiedDiffAnnotation) { + unifiedDiffAdditionCount++; + if (unifiedDiffAdditionCount > 1) { + throw new IllegalStateException("Multiple UnifiedDiffAnnotation for one UnifiedDiff found"); //$NON-NLS-1$ + } + Position pos = model.getPosition(lanno); + positions.add(pos); + replaceStrings.add(diff.rightStr); + } + model.removeAnnotation(lanno); + } + } + diffs1.clear(); + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + // we have to insert with delay because otherwise the line header code minings + // cannot be deleted + runAfterRepaintFinished(tw, () -> { + for (int i = positions.size() - 1; i >= 0; i--) { + Position pos = positions.get(i); + String replaceStr = replaceStrings.get(i); + try { + tv.getDocument().replace(pos.offset, pos.length, replaceStr); + } catch (BadLocationException e) { + UnifiedDiffManager.error(e); + } + } + }); + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/CancelAllRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/CancelAllRunnable.java new file mode 100644 index 00000000000..f3cf9df3a2b --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/CancelAllRunnable.java @@ -0,0 +1,44 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.disposeUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; + +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewerExtension5; + +public class CancelAllRunnable implements Runnable { + + private IAnnotationModel model; + private ITextViewer tv; + + public CancelAllRunnable(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + public String getLabel() { + return "Cancel All"; + } + + @Override + public void run() { + List diffs1 = get(tv); + for (UnifiedDiff diff : diffs1) { + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + for (var lanno : annos) { + model.removeAnnotation(lanno); + } + } + diffs1.clear(); + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/KeepAllRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/KeepAllRunnable.java new file mode 100644 index 00000000000..f33af1258fe --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/KeepAllRunnable.java @@ -0,0 +1,43 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.disposeUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; + +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewerExtension5; + +public class KeepAllRunnable implements Runnable { + private ITextViewer tv; + private IAnnotationModel model; + + public KeepAllRunnable(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + public String getLabel() { + return "Keep All"; + } + + @Override + public void run() { + List diffs1 = get(tv); + for (UnifiedDiff diff : diffs1) { + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + for (var lanno : annos) { + model.removeAnnotation(lanno); + } + } + diffs1.clear(); + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/NextRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/NextRunnable.java new file mode 100644 index 00000000000..1555337d551 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/NextRunnable.java @@ -0,0 +1,70 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getMinPositionAnno; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getToolbarShellForOneDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.selectAndRevealAnno; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.uncheckToolbarActionItems; + +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.swt.widgets.Shell; + +public class NextRunnable implements Runnable { + + private ToolBarManager tm; + private IAnnotationModel model; + private ITextViewer tv; + + public NextRunnable(ITextViewer tv, IAnnotationModel model, ToolBarManager tm) { + this.tv = tv; + this.model = model; + this.tm = tm; + } + + public String getLabel() { + return "Next"; + } + + @Override + public void run() { + uncheckToolbarActionItems(tm); + + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff == null) { + return; + } + if (!(tv.getSelectionProvider().getSelection() instanceof ITextSelection sel)) { + return; + } + int offset = sel.getOffset(); + List diffs1 = get(tv); + // get next UnifiedDiff for given offset + UnifiedDiff nextDiff = null; + for (UnifiedDiff diff : diffs1) { + if (diff.leftStart > offset) { + nextDiff = diff; + break; + } + if (nextDiff != null) { + break; + } + } + if (nextDiff == null) { + nextDiff = diffs1.get(0); + } + List all = getAllAnnotationsForUnifiedDiff(model, nextDiff); + if (all.size() == 0) { + return; + } + Annotation nextAnno = getMinPositionAnno(model, all); + selectAndRevealAnno(tv, model, nextAnno); + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/PreviousRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/PreviousRunnable.java new file mode 100644 index 00000000000..d56926fbed6 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/PreviousRunnable.java @@ -0,0 +1,66 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getMinPositionAnno; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getToolbarShellForOneDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.selectAndRevealAnno; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.uncheckToolbarActionItems; + +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.swt.widgets.Shell; + +public class PreviousRunnable implements Runnable { + + private ITextViewer tv; + private IAnnotationModel model; + private ToolBarManager tm; + + public PreviousRunnable(ITextViewer tv, IAnnotationModel model, ToolBarManager tm) { + this.tv = tv; + this.model = model; + this.tm = tm; + } + + public String getLabel() { + return "Previous"; + } + + @Override + public void run() { + uncheckToolbarActionItems(tm); + + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff == null) { + return; + } + if (!(tv.getSelectionProvider().getSelection() instanceof ITextSelection sel)) { + return; + } + int offset = sel.getOffset(); + List diffs1 = get(tv); + // get next UnifiedDiff for given offset + UnifiedDiff nextDiff = null; + for (UnifiedDiff diff : diffs1) { + if (diff.leftStart < offset) { + nextDiff = diff; + } + } + if (nextDiff == null) { + nextDiff = diffs1.getLast(); + } + List all = getAllAnnotationsForUnifiedDiff(model, nextDiff); + if (all.size() == 0) { + return; + } + Annotation nextAnno = getMinPositionAnno(model, all); + selectAndRevealAnno(tv, model, nextAnno); + } +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommand.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommand.java new file mode 100644 index 00000000000..812eca287ad --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommand.java @@ -0,0 +1,140 @@ +package org.eclipse.compare.unifieddiff.internal; + +import org.eclipse.compare.unifieddiff.UnifiedDiff; +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.MultiPageEditorPart; +import org.eclipse.ui.texteditor.ITextEditor; + +//TODO (tm) used for manual testing which will later be removed +public abstract class TestUnidiffCommand extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent arg0) throws ExecutionException { + IEditorPart ed = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor(); + if (ed instanceof MultiPageEditorPart mpe) { + Object p = mpe.getSelectedPage(); + if (p instanceof IEditorPart) { + ed = (IEditorPart) p; + } + } + if (ed instanceof ITextEditor ted) { + String r = """ + package jp1; + // TO BE ADDED BEGINNING + public class Test1 { + public void m1() { + System.err.bla("n"); + int i=0; + } + public static void main(String[] args) { + args[0]="a"; + } + } + // TO BE ADDED END + """; +// r = """ +// package jp1; +// +// public class Test1 { +// public void m1() { +// System.out.println("c"); +// System.out.println("d"); +// } +// public static void main(String[] args) { +// System.out.println("abb"); +// System.out.println("b"); +// } +// } +// """; + r = """ + package jp1; + + public class Test { + + public static void main(String[] args) { + // comment + m1(); + m1(); + m2(); + m3(); + m4(); + m5(); + m6(); + m7(); + m8(); + m9(); + m10(); + } + + private static void m5() { + // TODO Auto-generated method stub + + } + + private static void m6() { + // TODO Auto-generated method stub + + } + + private static void m7() { + // TODO Auto-generated method stub + + } + + private static void m8() { + println("a"); + println("b"); + + } + + private static void m9() { + // TODO Auto-generated method stub + + } + + private static void m10() { + // TODO Auto-generated method stub + + } + + static void m1() { + // com + } + + + static void m2() { + + } + + static void m3() { + + } + } + """; + r = """ + package jp1; + + public class Test1 { + public static void main(String[] args) { + System.out.println(sum(1,2) + max(2,1)); + } + + private static int sum(int first, int second) { + return first+second - third + "str"; + } + System.out.println("a");} + """; + + UnifiedDiff.create(ted, r, getMode()).open(); + } + return null; + } + + protected abstract UnifiedDiffMode getMode(); + +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayMode.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayMode.java new file mode 100644 index 00000000000..2154465bd39 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayMode.java @@ -0,0 +1,13 @@ +package org.eclipse.compare.unifieddiff.internal; + +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; + +// TODO (tm) used for manual testing which will later be removed +public class TestUnidiffCommandOverlayMode extends TestUnidiffCommand { + + @Override + protected UnifiedDiffMode getMode() { + return UnifiedDiffMode.OVERLAY_MODE; + } + +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayReadOnlyMode.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayReadOnlyMode.java new file mode 100644 index 00000000000..c768ad4d5d2 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandOverlayReadOnlyMode.java @@ -0,0 +1,13 @@ +package org.eclipse.compare.unifieddiff.internal; + +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; + +//TODO (tm) used for manual testing which will later be removed +public class TestUnidiffCommandOverlayReadOnlyMode extends TestUnidiffCommand { + + @Override + protected UnifiedDiffMode getMode() { + return UnifiedDiffMode.OVERLAY_READ_ONLY_MODE; + } + +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandReplaceMode.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandReplaceMode.java new file mode 100644 index 00000000000..7f86ae042f7 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/TestUnidiffCommandReplaceMode.java @@ -0,0 +1,13 @@ +package org.eclipse.compare.unifieddiff.internal; + +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; + +//TODO (tm) used for manual testing which will later be removed +public class TestUnidiffCommandReplaceMode extends TestUnidiffCommand { + + @Override + protected UnifiedDiffMode getMode() { + return UnifiedDiffMode.REPLACE_MODE; + } + +} diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UndoAllRunnable.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UndoAllRunnable.java new file mode 100644 index 00000000000..231f9b1b032 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UndoAllRunnable.java @@ -0,0 +1,75 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.disposeUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.error; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.get; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.getAllAnnotationsForUnifiedDiff; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.runAfterRepaintFinished; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiffAnnotation; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewerExtension5; + +public class UndoAllRunnable implements Runnable { + private ITextViewer tv; + private IAnnotationModel model; + + public UndoAllRunnable(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + public String getLabel() { + return "Undo All"; + } + + @Override + public void run() { + var tw = tv.getTextWidget(); + List diffs1 = get(tv); + List positions = new ArrayList<>(); + List replaceStrings = new ArrayList<>(); + for (UnifiedDiff diff : diffs1) { + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + int unifiedDiffAdditionCount = 0; + for (var lanno : annos) { + if (lanno instanceof UnifiedDiffAnnotation) { + unifiedDiffAdditionCount++; + if (unifiedDiffAdditionCount > 1) { + throw new IllegalStateException("Multiple UnifiedDiffAnnotation for one UnifiedDiff found"); //$NON-NLS-1$ + } + Position pos = model.getPosition(lanno); + positions.add(pos); + replaceStrings.add(diff.leftStr); + } + model.removeAnnotation(lanno); + } + } + diffs1.clear(); + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + // we have to insert with delay because otherwise the line header code minings + // cannot be deleted + runAfterRepaintFinished(tw, () -> { + for (int i = positions.size() - 1; i >= 0; i--) { + Position pos = positions.get(i); + String replaceStr = replaceStrings.get(i); + try { + tv.getDocument().replace(pos.offset, pos.length, replaceStr); + } catch (BadLocationException e) { + error(e); + } + } + }); + } +} 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 new file mode 100644 index 00000000000..15371387a5d --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffCodeMiningProvider.java @@ -0,0 +1,747 @@ +package org.eclipse.compare.unifieddiff.internal; + +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.error; +import static org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.isOverlay; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import org.eclipse.compare.unifieddiff.UnifiedDiff.UnifiedDiffMode; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager.UnifiedDiff; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.internal.text.codemining.CodeMiningDocumentFooterAnnotation; +import org.eclipse.jface.internal.text.codemining.CodeMiningLineHeaderAnnotation; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentExtension3; +import org.eclipse.jface.text.IDocumentPartitioner; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextPresentation; +import org.eclipse.jface.text.TextUtilities; +import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider; +import org.eclipse.jface.text.codemining.DocumentFooterCodeMining; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineHeaderCodeMining; +import org.eclipse.jface.text.presentation.IPresentationReconciler; +import org.eclipse.jface.text.presentation.IPresentationReconcilerExtension; +import org.eclipse.jface.text.presentation.IPresentationRepairer; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.SourceViewerConfiguration; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; +import org.eclipse.ui.texteditor.AbstractTextEditor; + +public class UnifiedDiffCodeMiningProvider extends AbstractCodeMiningProvider { + + private Color deletionBackgroundColor; + private Color detailedDiffColor; + private boolean lastIsOverlay; + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, + IProgressMonitor monitor) { + List diffs = UnifiedDiffManager.get(viewer); + if (diffs == null || diffs.size() == 0) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + boolean isOverlay = isOverlay(diffs); + if (this.deletionBackgroundColor == null || isOverlay != lastIsOverlay) { + // check class ColorPalette + RGB background = Display.getDefault().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(); + String colorName; + if (isOverlay) { + colorName = "ADDITION_COLOR"; //$NON-NLS-1$ + } else { + colorName = "DELETION_COLOR"; //$NON-NLS-1$ + } + RGB deletionColor = JFaceResources.getColorRegistry().getRGB(colorName); + this.detailedDiffColor = new Color(interpolate(deletionColor, background, 0.9)); + this.deletionBackgroundColor = new Color(interpolate(deletionColor, background, 0.8)); + lastIsOverlay = isOverlay; + // TODO (tm) remove me - add annotation extension with color 204, 229, 204 +// RGB additionColor = JFaceResources.getColorRegistry().getRGB("ADDITION_COLOR"); +// additionColor = interpolate(additionColor, background, 0.9); + } + if (viewer instanceof ISourceViewer sv && UnifiedDiffManager.get(viewer) != null) { + List existingMinings = new ArrayList<>(); + IAnnotationModel model = sv.getAnnotationModel(); + Iterator it = model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation next = it.next(); + if (next instanceof CodeMiningLineHeaderAnnotation n) { + try { + Field f = n.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(n); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffLineHeaderCodeMining idlhcm) { + Position p = n.getPosition(); + IDocument doc = sv.getDocument(); + int line = doc.getLineOfOffset(p.offset); + idlhcm.getPosition().offset = doc.getLineOffset(line); // we need to recalculate the line + // offset for the scenario where + // source is modified at the + // beginning of the line + existingMinings.add(idlhcm); + } + } catch (BadLocationException | NoSuchFieldException | SecurityException + | IllegalAccessException e) { + error(e); + } + } else if (next instanceof CodeMiningDocumentFooterAnnotation footer) { + try { + Field f = footer.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(footer); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffFooterCodeMining idlhcm) { + IDocument doc = sv.getDocument(); + idlhcm.getPosition().offset = doc.getLength(); + existingMinings.add(idlhcm); + } + } catch (NoSuchFieldException | SecurityException + | IllegalAccessException e) { + error(e); + } + } + } + if (existingMinings.size() > 0) { + return CompletableFuture.completedFuture(existingMinings); + } + } + + int tabWidth = getTabWidth(viewer); + return CompletableFuture.supplyAsync(() -> { + List minings = new ArrayList<>(); + createLineHeaderCodeMinings(diffs, minings, viewer, tabWidth); + return minings; + }); + } + + private int getTabWidth(ITextViewer viewer) { + int tabWidth = -1; + if (viewer != null && Display.getCurrent() != null) { + StyledText tw = viewer.getTextWidget(); + tabWidth = tw.getTabs(); + if (tabWidth > 0) { + return tabWidth; + } + } + AbstractTextEditor ate = getAbstractTextEditor(); + if (viewer != null && ate != null) { + ITextViewer a = ate.getAdapter(ITextViewer.class); + if (a == viewer && viewer instanceof ISourceViewer sv) { + try { + Field f = AbstractTextEditor.class.getDeclaredField("fConfiguration"); //$NON-NLS-1$ + f.setAccessible(true); + var config = (SourceViewerConfiguration) f.get(ate); + tabWidth = config.getTabWidth(sv); + } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException + | SecurityException e) { + error(e); + } + } + } + if (tabWidth == -1) { + IPreferenceStore store = EditorsUI.getPreferenceStore(); + tabWidth = store.getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH); + } + return tabWidth; + } + + private AbstractTextEditor getAbstractTextEditor() { + IWorkbench wb = PlatformUI.getWorkbench(); + if (wb == null) { + return null; + } + IWorkbenchWindow aww = wb.getActiveWorkbenchWindow(); + if (aww == null) { + return null; + } + IWorkbenchPage ap = aww.getActivePage(); + if (ap == null) { + return null; + } + IEditorPart ae = ap.getActiveEditor(); + if (ae instanceof AbstractTextEditor ate) { + return ate; + } + return null; + } + + private void createLineHeaderCodeMinings(List diffs, List minings, ITextViewer tv, + int tabWidth) { + if (diffs == null) { + return; + } + IDocument doc = tv.getDocument(); + for (UnifiedDiff diff : diffs) { + if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { + if (diff.leftStr.isEmpty()) { + continue; + } + try { + ICodeMining mining; + if (diff.leftStart == doc.getLength()) { + mining = new UnifiedDiffFooterCodeMining(doc, this, null, diff, tabWidth, + this.deletionBackgroundColor); + } else { + mining = new UnifiedDiffLineHeaderCodeMining(new Position(diff.leftStart, 1), this, diff, + tabWidth, this.detailedDiffColor, this.deletionBackgroundColor, tv); + } + minings.add(mining); + } catch (BadLocationException e) { + error(e); + } + } else if (diff.mode.equals(UnifiedDiffMode.OVERLAY_MODE) + || diff.mode.equals(UnifiedDiffMode.OVERLAY_READ_ONLY_MODE)) { + if (diff.rightStr.isEmpty()) { + continue; + } + try { + ICodeMining mining; + if (diff.leftStart == doc.getLength()) { + mining = new UnifiedDiffFooterCodeMining(doc, this, null, diff, tabWidth, + this.deletionBackgroundColor); + } else { + mining = new UnifiedDiffLineHeaderCodeMining(new Position(diff.leftStart + diff.leftLength, 1), + this, diff, tabWidth, this.detailedDiffColor, this.deletionBackgroundColor, tv); + } + minings.add(mining); + } catch (BadLocationException e) { + error(e); + } + } + } + } + + static class UnifiedDiffFooterCodeMining extends DocumentFooterCodeMining { + private final String unifiedDiffLabel; + private final Color deletionBackgroundColor; + private UnifiedDiff diff; + + public UnifiedDiffFooterCodeMining(IDocument document, ICodeMiningProvider provider, + Consumer action, UnifiedDiff diff, int tabWidth, Color deletionBackgroundColor) { + super(document, provider, action); + this.deletionBackgroundColor = deletionBackgroundColor; + if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { + this.unifiedDiffLabel = replaceTabWithSpaces(diff.leftStr, tabWidth); + } else { + this.unifiedDiffLabel = replaceTabWithSpaces(diff.rightStr, tabWidth); + } + this.diff = diff; + } + + @Override + public String getLabel() { + return this.unifiedDiffLabel; + } + + public UnifiedDiff getUnifiedDiff() { + return this.diff; + } + + @Override + public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { + gc.setBackground(this.deletionBackgroundColor); + Color c = textWidget.getForeground(); + gc.setForeground(c); + Font font = textWidget.getFont(); + gc.setFont(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); + // 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); + return result; + } + } + + public static class UnifiedDiffLineHeaderCodeMining extends LineHeaderCodeMining { + private final String unifiedDiffLabel; + private final Color deletionBackgroundColor; + private final Color detailedDiffColor; + private final UnifiedDiff diff; + private final int tabWidth; + private ITextViewer viewer; + private Rectangle lastRectangle; + + public UnifiedDiffLineHeaderCodeMining(Position position, ICodeMiningProvider provider, UnifiedDiff diff, + int tabWidth, Color deletionBackgroundColor, Color detailedDiffColor, ITextViewer viewer) + throws BadLocationException { + super(position, provider, new MouseClickConsumer(viewer)); + if (diff.mode.equals(UnifiedDiffMode.REPLACE_MODE)) { + this.unifiedDiffLabel = replaceTabWithSpaces(diff.leftStr, tabWidth); + } else { + this.unifiedDiffLabel = replaceTabWithSpaces(diff.rightStr, tabWidth); + } + this.deletionBackgroundColor = deletionBackgroundColor; + this.detailedDiffColor = detailedDiffColor; + this.diff = diff; + this.tabWidth = tabWidth; + this.viewer = viewer; + ((MouseClickConsumer) getAction()).setCodeMining(this); + } + + private static class MouseClickConsumer implements Consumer { + + private final ITextViewer viewer; + private UnifiedDiffLineHeaderCodeMining mining; + + public MouseClickConsumer(ITextViewer viewer) { + this.viewer = viewer; + } + + public void setCodeMining(UnifiedDiffLineHeaderCodeMining mining) { + this.mining = mining; + } + + @Override + public void accept(MouseEvent t) { + if (mining == null || viewer == null || mining.lastRectangle == null) { + return; + } + StyledText st = viewer.getTextWidget(); + StyledText overlay = new StyledText(st, SWT.NONE); + overlay.setBounds(mining.lastRectangle); + overlay.setFont(st.getFont()); + overlay.setBackground(mining.deletionBackgroundColor); + overlay.setText(mining.getLabel().stripTrailing()); + overlay.setFocus(); + // TODO (tm) style ranges missing + // TODO (tm) common keyboard shortcuts like ctrl-a, ctrl-right are not captured + // by this text control if in focus + overlay.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + overlay.dispose(); + } + }); + } + } + + @Override + public String getLabel() { + return this.unifiedDiffLabel; + } + + public UnifiedDiff getUnifiedDiff() { + return this.diff; + } + + @Override + public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { + gc.setBackground(this.deletionBackgroundColor); + Color c = textWidget.getForeground(); + gc.setForeground(c); + Font font = textWidget.getFont(); + gc.setFont(font); + // first run to get width and height for label + 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); + + String label = getLabel(); + List ranges = getStyleRanges(viewer, label); + HashMap> styledFonts = new HashMap<>(); + + // draw darker background for detailed diff + gc.setBackground(this.detailedDiffColor); + for (var detailedDiff : this.diff.detailedDiffs) { + String diffStr = this.diff.leftStr; + String detailedDiffStr = detailedDiff.leftStr; + int detailedDiffStart = detailedDiff.leftStart; + int detailedDiffLength = detailedDiff.leftLength; + if (diff.mode.equals(UnifiedDiffMode.OVERLAY_MODE) + || diff.mode.equals(UnifiedDiffMode.OVERLAY_READ_ONLY_MODE)) { + diffStr = this.diff.rightStr; + detailedDiffStr = detailedDiff.rightStr; + detailedDiffStart = detailedDiff.rightStart; + detailedDiffLength = detailedDiff.rightLength; + } + if (detailedDiffStr.trim().length() == 0) { + continue; + } + try { + String l = diffStr.substring(0, detailedDiffStart); + int fromLine = l.split("\n").length; //$NON-NLS-1$ + int toLine = diffStr.substring(0, detailedDiffStart + detailedDiffLength).split("\n").length; //$NON-NLS-1$ + if (fromLine == toLine) { + int starty = getYForLine(fromLine - 1, y, gc, textWidget); + Point start = getPositionForOffset(gc, detailedDiffStart, diffStr, ranges, styledFonts); + Point curr = getPositionForOffset(gc, detailedDiffStart + detailedDiffLength, diffStr, ranges, + styledFonts); + gc.fillRectangle(x + start.x, starty, curr.x - start.x, curr.y); + } else { + // mark first line until end + String[] lines = diffStr.split("\n"); //$NON-NLS-1$ + String firstLine = lines[fromLine - 1]; + int starty = getYForLine(fromLine - 1, y, gc, textWidget); + int idx = getOffsetAtLine(diffStr, detailedDiffStart); + Point start = getPositionForOffset(gc, idx, diffStr, ranges, styledFonts); + Point curr = getPositionForOffset(gc, firstLine.length(), diffStr, ranges, styledFonts); + if (curr.x > 0) { + gc.fillRectangle(x + start.x, starty, curr.x - start.x, curr.y); + } + // all the lines between first and last line + var diffStrDoc = new Document(diffStr); + for (int middleLine = fromLine + 1; middleLine < toLine; middleLine++) { + starty = getYForLine(middleLine - 1, y, gc, textWidget); + try { + int currentLineEndOffset = diffStrDoc.getLineOffset(middleLine - 1) + + diffStrDoc.getLineLength(middleLine - 1) + - getLineDelimiterLength(diffStrDoc, middleLine); + curr = getPositionForOffset(gc, currentLineEndOffset, diffStr, ranges, styledFonts); + if (curr.x > 0) { + gc.fillRectangle(x, starty, curr.x, curr.y); + } + } catch (BadLocationException e) { + error(e); + } + } + // last line + starty = getYForLine(toLine - 1, y, gc, textWidget); + curr = getPositionForOffset(gc, detailedDiffStart + detailedDiffLength, diffStr, ranges, + styledFonts); + if (curr.x > 0) { + gc.fillRectangle(x, starty, curr.x, curr.y); + } + } + } catch (IllegalArgumentException e) { + error(e); + } + } + // draw foreground again + int cx = x; + int cy = y; + try { + 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, styledFonts); + 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); + Point p = gc.stringExtent(line); + if (i < lines.length - 1) { + cy += p.y + textWidget.getLineSpacing(); + cx = x; + } else { + cx += p.x; + } + } + } else { + gc.drawString(sub, cx, cy, true); + Point p = gc.stringExtent(sub); + if (sub.endsWith("\n")) { //$NON-NLS-1$ + cy += p.y + 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 * (p.y + textWidget.getLineSpacing()); + cx = x; + } + cx += p.x; + } + } + } finally { + styledFonts.forEach((font1, styledFonts1) -> { + styledFonts1.forEach((style, styledFont) -> { + styledFont.dispose(); + }); + styledFonts1.clear(); + }); + } + return result; + } + + private int getLineDelimiterLength(Document diffStrDoc, int middleLine) throws BadLocationException { + String delim = diffStrDoc.getLineDelimiter(middleLine - 1); + if (delim == null) { + return 0; + } + return delim.length(); + } + + private Point getPositionForOffset(GC gc, int offset, String str, List ranges, + HashMap> styledFonts) { + String sub = str.substring(0, offset); + int tabCount = sub.split("\t", -1).length - 1; //$NON-NLS-1$ + offset += tabCount * tabWidth - tabCount; + str = replaceTabWithSpaces(str, tabWidth); + Point result = null; + var before = gc.getFont(); + try { + for (var range : ranges) { + if (range.start <= offset) { + int rangeEnd = range.start + range.length; + if (rangeEnd > offset) { + rangeEnd = offset; + } + sub = str.substring(range.start, rangeEnd); + if (offset == rangeEnd && offset == str.length()) { + while (sub.endsWith("\n")) { //$NON-NLS-1$ + sub = sub.substring(0, sub.length() - 1); + } + } + int lfIdx = sub.lastIndexOf("\n"); //$NON-NLS-1$ + if (lfIdx > 0) { + sub = sub.substring(lfIdx + 1); + result = null; + } + var rangeWithFont = transformFontStyleToFont(before, range, styledFonts); + if (rangeWithFont.font != null) { + gc.setFont(rangeWithFont.font); + } else { + gc.setFont(before); + } + Point extent = gc.stringExtent(sub); + if (result == null) { + result = extent; + } else { + result.x += extent.x; + } + } else { + break; + } + } + } finally { + gc.setFont(before); + } + return result; + } + + private StyleRange transformFontStyleToFont(Font baseFont, StyleRange styleRange, + HashMap> styledFonts) { + // 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; + } + + private List getStyleRanges(ITextViewer v, String source) { + List result = new ArrayList<>(); + if (!(v instanceof SourceViewer sv)) { + return result; + } + try { + // TODO (tm) API needed to access IPresentationReconciler + Field f = SourceViewer.class.getDeclaredField("fPresentationReconciler"); //$NON-NLS-1$ + f.setAccessible(true); + var reconciler = (IPresentationReconciler) f.get(sv); + // TODO (tm) should we better cache the presentation and don't calculate it each + // time? + result = createPresentation(reconciler, sv.getDocument(), source); + } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException e) { + error(e); + } + return result; + } + + private List createPresentation(IPresentationReconciler reconciler, IDocument originalDocument, + String source) { + try { + String prefix = originalDocument.get(0, diff.leftStart); + IDocument document = new Document(prefix + source); + IRegion damage = new Region(prefix.length(), source.length()); + String partition = IDocumentExtension3.DEFAULT_PARTITIONING; + if (reconciler instanceof IPresentationReconcilerExtension ext) { + String extPartition = ext.getDocumentPartitioning(); + if (extPartition != null && !extPartition.isEmpty()) { + partition = extPartition; + } + } + IDocumentPartitioner partitioner = originalDocument.getDocumentPartitioner(); + document.setDocumentPartitioner(partitioner); + IDocumentPartitioner originalDocumentPartitioner = null; + if (document instanceof IDocumentExtension3 ext + && originalDocument instanceof IDocumentExtension3 originalExt) { + originalDocumentPartitioner = originalExt.getDocumentPartitioner(partition); + if (originalDocumentPartitioner != null) { + // set temporarily another document in partitioner so that presentation can be + // created for given source + originalDocumentPartitioner.disconnect(); + originalDocumentPartitioner.connect(document); + ext.setDocumentPartitioner(partition, originalDocumentPartitioner); + } + } + TextPresentation presentation = new TextPresentation(damage, 1000); + + ITypedRegion[] partitioning = TextUtilities.computePartitioning(document, partition, damage.getOffset(), + damage.getLength(), false); + for (ITypedRegion r : partitioning) { + IPresentationRepairer repairer = reconciler.getRepairer(r.getType()); + if (repairer != null) { + repairer.setDocument(document); + repairer.createPresentation(presentation, r); + repairer.setDocument(originalDocument); + } + } + if (originalDocumentPartitioner != null) { + originalDocumentPartitioner.connect(originalDocument); + } + List result = new ArrayList<>(); + var it = presentation.getAllStyleRangeIterator(); + int startOffset = prefix.length(); + while (it.hasNext()) { + StyleRange next = it.next(); + if (next.start < startOffset) { + throw new IllegalStateException( + "Invalid presentation with style range starting before source offset"); //$NON-NLS-1$ + } + next.start -= startOffset; + result.add(next); + } + return result; + } catch (BadLocationException x) { + return null; + } + } + + private int getOffsetAtLine(String str, int off) { + Document doc; + if (off == str.length()) { + doc = new Document(str); + } else { + doc = new Document(str.stripTrailing()); // TODO (tm) strange: when do we need to stripTrailing? + } + try { + if (off > doc.getLength()) { + // TODO (tm) don't get it - when is this branch needed + int line = doc.getLineOfOffset(doc.getLength()); + int lineOffset = doc.getLineOffset(line); + int resultOff = doc.getLength() - lineOffset; + return resultOff; + } + int line = doc.getLineOfOffset(off); + int lineOffset = doc.getLineOffset(line); + int resultOff = off - lineOffset; + return resultOff; + } catch (BadLocationException e) { + error(e); + } + return -1; + } + + private int getYForLine(int line, int y, GC gc, StyledText textWidget) { + Point ext = gc.stringExtent("A"); //$NON-NLS-1$ + y += line * (ext.y + textWidget.getLineSpacing()); + return y; + } + } + + // 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 + } + +// private static String getStringFromLastLine(String str) { +// int idx = str.lastIndexOf("\n"); //$NON-NLS-1$ +// if (idx < 0) { +// return str; +// } +// str = str.substring(idx + 1); +// return str; +// } + + private static String replaceTabWithSpaces(String leftStr, int tabWidth) { + if (leftStr == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int s = 0; s < tabWidth; s++) { + sb.append(' '); + } + String result = leftStr.replace("\t", sb); //$NON-NLS-1$ + return result; + } +} 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 new file mode 100644 index 00000000000..51924138ad9 --- /dev/null +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java @@ -0,0 +1,1408 @@ +package org.eclipse.compare.unifieddiff.internal; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.compare.contentmergeviewer.IIgnoreWhitespaceContributor; +import org.eclipse.compare.contentmergeviewer.ITokenComparator; +import org.eclipse.compare.contentmergeviewer.TokenComparator; +import org.eclipse.compare.internal.DocLineComparator; +import org.eclipse.compare.rangedifferencer.IRangeComparator; +import org.eclipse.compare.rangedifferencer.RangeDifference; +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.UnifiedDiffMode; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffFooterCodeMining; +import org.eclipse.compare.unifieddiff.internal.UnifiedDiffCodeMiningProvider.UnifiedDiffLineHeaderCodeMining; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IWorkspaceRunnable; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IContributionItem; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.internal.text.codemining.CodeMiningDocumentFooterAnnotation; +import org.eclipse.jface.internal.text.codemining.CodeMiningLineHeaderAnnotation; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModelEvent; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelListener; +import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; +import org.eclipse.jface.text.source.ISourceViewerExtension5; +import org.eclipse.jface.text.source.projection.ProjectionViewer; +import org.eclipse.jface.util.Geometry; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.text.undo.DocumentUndoManagerRegistry; +import org.eclipse.text.undo.IDocumentUndoListener; +import org.eclipse.text.undo.IDocumentUndoManager; +import org.eclipse.ui.texteditor.ITextEditor; + +public class UnifiedDiffManager { + + private static final String CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY = "CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY"; //$NON-NLS-1$ + private static final String UNDO_LISTENER_KEY = "UNIFIED_DIFF_UNDO_LISTENER_KEY"; //$NON-NLS-1$ + private static final String UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY = "UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY"; //$NON-NLS-1$ + private static final String ADDITION_ANNO_TYPE = "org.eclipse.compare.unifieddiff.internal.addition"; //$NON-NLS-1$ + private static final String DELETION_ANNO_TYPE = "org.eclipse.compare.unifieddiff.internal.deletion"; //$NON-NLS-1$ + private static final String DETAILED_ADDITION_ANNO_TYPE = "org.eclipse.compare.unifieddiff.internal.detailedAddition"; //$NON-NLS-1$ + private static final String DETAILED_DELETION_ANNO_TYPE = "org.eclipse.compare.unifieddiff.internal.detailedDeletion"; //$NON-NLS-1$ + private static final String TOOLBAR_SHELL_FOR_ONE_DIFF_KEY = "TOOLBAR_SHELL_FOR_ONE_DIFF_KEY"; //$NON-NLS-1$ + private static final String TOOLBAR_SHELL_FOR_ALL_DIFFS_KEY = "TOOLBAR_SHELL_FOR_ALL_DIFFS_KEY"; //$NON-NLS-1$ + private static final Map> diffsByViewer = new HashMap<>(); + + public static void put(ITextViewer viewer, List diffs) { + diffsByViewer.put(viewer, diffs); + } + + public static List get(ITextViewer viewer) { + return diffsByViewer.get(viewer); + } + + public static void open(ITextEditor editor, String source, UnifiedDiffMode mode, List additionalActions, + TokenComparatorFactory tokenComparatorFactory, + IgnoreWhitespaceContributorFactory ignoreWhitespaceContributorFactory, boolean ignoreWhiteSpace) { + ITextViewer viewer = editor.getAdapter(ITextViewer.class); + if (viewer instanceof ProjectionViewer pv) { + pv.doOperation(ProjectionViewer.EXPAND_ALL); + } + IAnnotationModel model = editor.getDocumentProvider().getAnnotationModel(editor.getEditorInput()); + clearAll(viewer, model); + + IDocument leftDocument = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + IDocument rightDocument = new Document(source); + + DocLineComparator left = null, right = null; + Optional lDocIgnonerWhitespaceContributor = Optional.empty(); + Optional rDocIgnonreWhitespaceContributor = Optional.empty(); + if (ignoreWhitespaceContributorFactory != null) { + lDocIgnonerWhitespaceContributor = ignoreWhitespaceContributorFactory.apply(leftDocument); + left = new DocLineComparator(leftDocument, null, ignoreWhiteSpace, null, '?', + lDocIgnonerWhitespaceContributor); + rDocIgnonreWhitespaceContributor = ignoreWhitespaceContributorFactory.apply(rightDocument); + right = new DocLineComparator(rightDocument, null, ignoreWhiteSpace, null, '?', + rDocIgnonreWhitespaceContributor); + } else { + left = new DocLineComparator(leftDocument, null, ignoreWhiteSpace); + right = new DocLineComparator(rightDocument, null, ignoreWhiteSpace); + } + List unifiedDiffs = new ArrayList<>(); + RangeDifference[] rangeDiffs = RangeDifferencer.findDifferences(left, right); + for (RangeDifference rangeDiff : rangeDiffs) { + try { + int leftStart = left.getTokenStart(rangeDiff.leftStart()); + int leftEnd = getTokenEnd(left, rangeDiff.leftStart(), rangeDiff.leftLength()); + String leftDiffSource = leftDocument.get(leftStart, leftEnd - leftStart); + + int rightStart = right.getTokenStart(rangeDiff.rightStart()); + int rightEnd = getTokenEnd(right, rangeDiff.rightStart(), rangeDiff.rightLength()); + String rightDiffSource = rightDocument.get(rightStart, rightEnd - rightStart); + + if (leftDiffSource.length() == 0 && rightDiffSource.length() == 0) { + continue; + } + boolean isWhitespace = false; + // Indicate whether all contributors are whitespace + if (ignoreWhiteSpace && leftDiffSource.trim().length() == 0 && rightDiffSource.trim().length() == 0) { + isWhitespace = true; + + // Check if whitespace can be ignored by the contributor + if (leftDiffSource.length() > 0 && !lDocIgnonerWhitespaceContributor.isEmpty()) { + boolean isIgnored = lDocIgnonerWhitespaceContributor.get() + .isIgnoredWhitespace(rangeDiff.leftStart(), rangeDiff.leftLength()); + isWhitespace = isIgnored; + } + if (isWhitespace && rightDiffSource.length() > 0 && !rDocIgnonreWhitespaceContributor.isEmpty()) { + boolean isIgnored = rDocIgnonreWhitespaceContributor.get() + .isIgnoredWhitespace(rangeDiff.rightStart(), rangeDiff.rightLength()); + isWhitespace = isIgnored; + } + } + if (isWhitespace) { + continue; + } + int kind = rangeDiff.kind(); + switch (kind) { + case RangeDifference.NOCHANGE: + break; + case RangeDifference.CHANGE: + var diff = new UnifiedDiff(leftDocument, leftStart, leftEnd, leftDiffSource, rightDocument, + rightStart, rightEnd, rightDiffSource, unifiedDiffs, mode); + unifiedDiffs.add(diff); + + // line based fine granular diff via DocumentMerger#simpleTokenDiff + ITokenComparator l = createTokenComparator(leftDiffSource, tokenComparatorFactory); + ITokenComparator r = createTokenComparator(rightDiffSource, tokenComparatorFactory); + RangeDifference[] detailedDiffs = RangeDifferencer.findRanges((IRangeComparator) null, l, r); + for (RangeDifference detailedDiff : detailedDiffs) { + if (detailedDiff.kind() == RangeDifference.NOCHANGE) { + continue; + } + int detailedLeftStart = l.getTokenStart(detailedDiff.leftStart()); + int detailedLeftEnd = getTokenEnd(l, detailedDiff.leftStart(), detailedDiff.leftLength()); + String detailedLeftDiffSource = leftDiffSource.substring(detailedLeftStart, detailedLeftEnd); + + int detailedRightStart = r.getTokenStart(detailedDiff.rightStart()); + int detailedRightEnd = getTokenEnd(r, detailedDiff.rightStart(), detailedDiff.rightLength()); + String detailedDiffRightDiffSource = rightDiffSource.substring(detailedRightStart, + detailedRightEnd); + if (detailedLeftDiffSource.trim().length() == 0 + && detailedDiffRightDiffSource.trim().length() == 0) { + continue; + } + diff.detailedDiffs.add(new UnifiedDiff(leftDocument, detailedLeftStart, detailedLeftEnd, + detailedLeftDiffSource, rightDocument, detailedRightStart, detailedRightEnd, + detailedDiffRightDiffSource, unifiedDiffs, mode)); + } + break; + case RangeDifference.CONFLICT: + break; + case RangeDifference.LEFT: + break; + case RangeDifference.ERROR: + break; + case RangeDifference.ANCESTOR: + break; + default: + break; + } + } catch (BadLocationException e) { + error(e); + } + } + + // call validateEdit before modifying the leftDocument + IFile file = editor.getEditorInput().getAdapter(IFile.class); + if (file != null && !validateEdit(file)) { + return; + } + + Map diffByAnno = new HashMap<>(); + if (mode.equals(UnifiedDiffMode.REPLACE_MODE)) { + // modify document + int delta = 0; + for (UnifiedDiff unifiedDiff : unifiedDiffs) { + try { + unifiedDiff.leftStart += delta; + leftDocument.replace(unifiedDiff.leftStart, unifiedDiff.leftLength, unifiedDiff.rightStr); + delta += (unifiedDiff.rightLength - unifiedDiff.leftLength); + Annotation myAnnotation = new UnifiedDiffAnnotation(mode, unifiedDiff); + Position position = new Position(unifiedDiff.leftStart, unifiedDiff.rightLength); + // TODO (tm) question: should we better use mass API here to add all annotations + // in one run? + model.addAnnotation(myAnnotation, position); + for (var detailedDiff : unifiedDiff.detailedDiffs) { + if (detailedDiff.rightStr.trim().length() == 0) { + continue; + } + Annotation detailedAnno = new DetailedDiffAnnotation(mode, unifiedDiff); + Position detailedPos = new Position(unifiedDiff.leftStart + detailedDiff.rightStart, + detailedDiff.rightLength); + model.addAnnotation(detailedAnno, detailedPos); + } + diffByAnno.put(myAnnotation, unifiedDiff); + } catch (BadLocationException e) { + error(e); + } + } + } else { + for (UnifiedDiff unifiedDiff : unifiedDiffs) { + Annotation myAnnotation = new UnifiedDiffAnnotation(mode, unifiedDiff); + Position position = new Position(unifiedDiff.leftStart, unifiedDiff.leftLength); + model.addAnnotation(myAnnotation, position); + for (var detailedDiff : unifiedDiff.detailedDiffs) { + if (detailedDiff.leftStr.trim().length() == 0) { + continue; + } + Annotation detailedAnno = new DetailedDiffAnnotation(mode, unifiedDiff); + Position detailedPos = new Position(unifiedDiff.leftStart + detailedDiff.leftStart, + detailedDiff.leftLength); + model.addAnnotation(detailedAnno, detailedPos); + } + diffByAnno.put(myAnnotation, unifiedDiff); + } + } + + UnifiedDiffManager.put(viewer, unifiedDiffs); + addPaintListener(viewer, model, mode); + addMouseMoveListener(viewer, model); + if (viewer instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + drawToolBarForAllDiffs(viewer, model, additionalActions, mode); + addUndoListener(viewer, leftDocument, model); + addAnnoModelChangeListener(viewer, model); + // TODO (tm) focus listener to hide toolbar might be not the best idea - part + // listener better? + addFocusListener(viewer); + + if (unifiedDiffs.size() > 0) { + runAfterRepaintFinished(viewer.getTextWidget(), () -> { + Annotation firstAnno = getFirstAnnotationForUnifiedDiff(model, unifiedDiffs.get(0)); + selectAndRevealAnno(viewer, model, firstAnno); + }); + } + + // TODO (tm) line spacing 60% does not look good - should be fixed now. Check it + // again. + } + + private static void addFocusListener(ITextViewer viewer) { + var tw = viewer.getTextWidget(); + Stream stream = tw.getTypedListeners(SWT.FocusIn, UnifiedDiffFocusListener.class); + if (stream != null && stream.count() > 0) { + return; + } + tw.addFocusListener(new UnifiedDiffFocusListener(viewer)); + } + + private static final class UnifiedDiffFocusListener implements FocusListener { + + private final ITextViewer tv; + + public UnifiedDiffFocusListener(ITextViewer tv) { + this.tv = tv; + } + + @Override + public void focusGained(FocusEvent e) { + Display.getDefault().asyncExec(() -> { + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff != null && !fToolbarShellForOneDiff.isDisposed() + && !fToolbarShellForOneDiff.isVisible()) { + fToolbarShellForOneDiff.setVisible(true); + } + var fToolbarShellForAllDiffs = getToolbarShellForAllDiffs(tv.getTextWidget()); + if (fToolbarShellForAllDiffs != null && !fToolbarShellForAllDiffs.isDisposed() + && !fToolbarShellForAllDiffs.isVisible()) { + fToolbarShellForAllDiffs.setVisible(true); + } + }); + } + + @Override + public void focusLost(FocusEvent e) { + // without asynExec the toolbar button do no not react to click events - make + // them invisible with a small delay + Display.getDefault().asyncExec(() -> { + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff != null && !fToolbarShellForOneDiff.isDisposed() + && fToolbarShellForOneDiff.isVisible()) { + fToolbarShellForOneDiff.setVisible(false); + } + var fToolbarShellForAllDiffs = getToolbarShellForAllDiffs(tv.getTextWidget()); + if (fToolbarShellForAllDiffs != null && !fToolbarShellForAllDiffs.isDisposed() + && fToolbarShellForAllDiffs.isVisible()) { + fToolbarShellForAllDiffs.setVisible(false); + } + }); + } + } + + static Shell getToolbarShellForOneDiff(StyledText tw) { + if (tw == null) { + return null; + } + var result = (Shell) tw.getData(TOOLBAR_SHELL_FOR_ONE_DIFF_KEY); + return result; + } + + private static Shell getToolbarShellForAllDiffs(StyledText tw) { + if (tw == null) { + return null; + } + var result = (Shell) tw.getData(TOOLBAR_SHELL_FOR_ALL_DIFFS_KEY); + return result; + } + + private static void addAnnoModelChangeListener(ITextViewer tv, IAnnotationModel model) { + // ensure not registered multiple times + var listener = (UnifiedDiffAnnotationmodelListener) tv.getTextWidget() + .getData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY); + if (listener != null) { + return; + } + // we need to remove the UnifiedDiffs in the range when the user deletes the + // ranges manually via keyboard - listen to annotation model changes + listener = new UnifiedDiffAnnotationmodelListener(tv); + tv.getTextWidget().setData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY, listener); + model.addAnnotationModelListener(listener); + } + + private static final class UnifiedDiffAnnotationmodelListener + implements IAnnotationModelListener, IAnnotationModelListenerExtension { + + private final ITextViewer tv; + + public UnifiedDiffAnnotationmodelListener(ITextViewer tv) { + this.tv = tv; + } + + @Override + public void modelChanged(AnnotationModelEvent event) { + Annotation[] annos = event.getRemovedAnnotations(); + if (annos == null) { + return; + } + List unifiedDiffsToDelete = new ArrayList<>(); + for (var anno : annos) { + UnifiedDiff unifiedDiff = getUnifiedDiffForAnno(anno); + if (unifiedDiff != null) { + unifiedDiffsToDelete.add(unifiedDiff); + } + } + if (unifiedDiffsToDelete.size() > 0) { + Display.getDefault().asyncExec(() -> { + UnifiedDiff currentToolbarUnifiedDiff = null; + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff != null) { + var currentToolbarUnifiedDiffAnno = (Annotation) toolbarShellForOneDiff + .getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + currentToolbarUnifiedDiff = getUnifiedDiffForAnno(currentToolbarUnifiedDiffAnno); + } + List container = null; + IAnnotationModel model = event.getAnnotationModel(); + for (var unifiedDiff : unifiedDiffsToDelete) { + List annos1 = getAllAnnotationsForUnifiedDiff(model, unifiedDiff); + for (var lanno : annos1) { + model.removeAnnotation(lanno); + } + if (currentToolbarUnifiedDiff != null && currentToolbarUnifiedDiff.equals(unifiedDiff)) { + disposeToolbarForOneDiff(toolbarShellForOneDiff, + UnifiedDiffAnnotationmodelListener.this.tv); + } + unifiedDiff.container.remove(unifiedDiff); + container = unifiedDiff.container; + } + if (UnifiedDiffAnnotationmodelListener.this.tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + if (container != null && container.size() == 0) { + disposeUnifiedDiff(UnifiedDiffAnnotationmodelListener.this.tv, model, tv.getTextWidget()); + } + }); + } + } + + @Override + public void modelChanged(IAnnotationModel model) { + } + } + + public static void error(Exception e) { + Platform.getLog(UnifiedDiffManager.class).error(e.getMessage(), e); + } + + static final class UnifiedDiffAnnotation extends Annotation { + private final UnifiedDiff unifiedDiff; + + public UnifiedDiffAnnotation(UnifiedDiffMode mode, UnifiedDiff unifiedDiff) { + super(UnifiedDiffMode.REPLACE_MODE.equals(mode) ? ADDITION_ANNO_TYPE : DELETION_ANNO_TYPE, false, null); + this.unifiedDiff = unifiedDiff; + } + + public UnifiedDiff getUnifiedDiff() { + return this.unifiedDiff; + } + } + + private static final class DetailedDiffAnnotation extends Annotation { + private final UnifiedDiff unifiedDiff; + + public DetailedDiffAnnotation(UnifiedDiffMode mode, UnifiedDiff unifiedDiff) { + super(UnifiedDiffMode.REPLACE_MODE.equals(mode) ? DETAILED_ADDITION_ANNO_TYPE : DETAILED_DELETION_ANNO_TYPE, + false, null); + this.unifiedDiff = unifiedDiff; + } + + public UnifiedDiff getUnifiedDiff() { + return this.unifiedDiff; + } + } + + private static void drawToolBarForAllDiffs(ITextViewer tv, IAnnotationModel model, List additionalActions, + UnifiedDiffMode mode) { + StyledText tw = tv.getTextWidget(); + Shell parentShell = tw.getShell(); + int shellStyle = SWT.TOOL; + shellStyle &= ~(SWT.NO_TRIM | SWT.SHELL_TRIM); // make sure we get the OS border but no other trims + var tm = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT); + List diffs = get(tv); + if (UnifiedDiffMode.OVERLAY_MODE.equals(mode) || UnifiedDiffMode.OVERLAY_READ_ONLY_MODE.equals(mode)) { + if (!isReadOnly(diffs)) { + var acceptAll = new AcceptAllRunnable(tv, model); + addToolbarAction(tm, acceptAll.getLabel(), acceptAll); + } + var cancelAll = new CancelAllRunnable(tv, model); + addToolbarAction(tm, cancelAll.getLabel(), cancelAll); + } else { + var keepAll = new KeepAllRunnable(tv, model); + addToolbarAction(tm, keepAll.getLabel(), keepAll); + var undoAll = new UndoAllRunnable(tv, model); + addToolbarAction(tm, undoAll.getLabel(), undoAll); + } + + var previous = new PreviousRunnable(tv, model, tm); + addToolbarAction(tm, previous.getLabel(), previous); + var next = new NextRunnable(tv, model, tm); + addToolbarAction(tm, next.getLabel(), next); + if (additionalActions != null) { + for (var additionalAction : additionalActions) { + addToolbarAction(tm, additionalAction); + } + } + if (tm.isEmpty()) { + return; + } + var toolbarShellForAllDiffs = getToolbarShellForAllDiffs(tv.getTextWidget()); + if (toolbarShellForAllDiffs != null && !toolbarShellForAllDiffs.isDisposed()) { + disposeToolbarForAllDiffs(toolbarShellForAllDiffs, tv); + } + toolbarShellForAllDiffs = new Shell(parentShell, shellStyle); + tw.setData(TOOLBAR_SHELL_FOR_ALL_DIFFS_KEY, toolbarShellForAllDiffs); + var layout = new GridLayout(1, false); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 0; + toolbarShellForAllDiffs.setLayout(layout); + + var fContentComposite = new Composite(toolbarShellForAllDiffs, SWT.NONE); + fContentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + fContentComposite.setLayout(new FillLayout()); + + ToolBar fToolBar = tm.createControl(fContentComposite); + GridData gd = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); + fToolBar.setLayoutData(gd); + + toolbarShellForAllDiffs.pack(); + + var setLocationRunnable = (Runnable) () -> { + var toolbarShellForAllDiffs1 = getToolbarShellForAllDiffs(tv.getTextWidget()); + if (toolbarShellForAllDiffs1 == null || toolbarShellForAllDiffs1.isDisposed()) { + return; + } + Rectangle clientArea = tw.getClientArea(); + Rectangle twBounds = tw.getBounds(); + Geometry.moveInside(twBounds, clientArea); + Rectangle relativeTwBounds = Geometry.toDisplay(tw, twBounds); + toolbarShellForAllDiffs1.setLocation( + relativeTwBounds.x + relativeTwBounds.width / 2 - toolbarShellForAllDiffs1.getBounds().width / 2, + relativeTwBounds.y + relativeTwBounds.height - toolbarShellForAllDiffs1.getBounds().height); + }; + setLocationRunnable.run(); + toolbarShellForAllDiffs.setVisible(true); + + tw.addControlListener(new ControlListener() { + + @Override + public void controlResized(ControlEvent e) { + setLocationRunnable.run(); + } + + @Override + public void controlMoved(ControlEvent e) { + } + }); + + Stream stream = tw.getTypedListeners(SWT.Dispose, + ToolbarDisposeListenerForOneAndAllDiffs.class); + if (stream == null || stream.count() == 0) { + tw.addDisposeListener(new ToolbarDisposeListenerForOneAndAllDiffs(tv, model)); + } + } + + private static void clearAll(ITextViewer tv, IAnnotationModel model) { + List diffs1 = get(tv); + if (diffs1 == null) { + return; + } + StyledText tw = tv.getTextWidget(); + var listener = (UnifiedDiffAnnotationmodelListener) tw.getData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY); + if (listener != null) { + tw.setData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY, null); + model.removeAnnotationModelListener(listener); + } + for (UnifiedDiff diff : diffs1) { + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + for (var lanno : annos) { + model.removeAnnotation(lanno); + } + } + diffs1.clear(); + } + + static void uncheckToolbarActionItems(ToolBarManager tm) { + IContributionItem[] items = tm.getItems(); + if (items == null) { + return; + } + for (IContributionItem item : items) { + if (item instanceof ActionContributionItem actionItem) { + IAction action = actionItem.getAction(); + if (action == null) { + continue; + } + action.setChecked(false); + } + } + } + + public static boolean isOverlay(List diffs) { + if (diffs != null && diffs.size() > 0) { + return diffs.get(0).mode.equals(UnifiedDiffMode.OVERLAY_MODE) + || diffs.get(0).mode.equals(UnifiedDiffMode.OVERLAY_READ_ONLY_MODE); + } + return false; + } + + public static boolean isReadOnly(List diffs) { + if (diffs != null && diffs.size() > 0) { + return diffs.get(0).mode.equals(UnifiedDiffMode.OVERLAY_READ_ONLY_MODE); + } + return false; + } + + private static void drawToolbarForOneDiff(ITextViewer tv, IAnnotationModel model) { + List diffs = get(tv); + StyledText tw = tv.getTextWidget(); + Shell parentShell = tw.getShell(); + int shellStyle = SWT.TOOL; + shellStyle &= ~(SWT.NO_TRIM | SWT.SHELL_TRIM); // make sure we get the OS border but no other trims + var tm = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT); + if (isOverlay(diffs)) { + if (!isReadOnly(diffs)) { + addToolbarAction(tm, "Accept", () -> { + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff == null) { + return; + } + var anno = (Annotation) toolbarShellForOneDiff.getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + if (anno == null) { + return; + } + UnifiedDiff diff = getUnifiedDiffForAnno(anno); + if (diff == null) { + return; + } + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + List positions = new ArrayList<>(); + List replaceStrings = new ArrayList<>(); + for (var lanno : annos) { + if (lanno instanceof UnifiedDiffAnnotation) { + Position pos = model.getPosition(lanno); + positions.add(pos); + replaceStrings.add(diff.rightStr); + } + model.removeAnnotation(lanno); + } + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + // we have to insert with delay because otherwise the line header code minings + // cannot be deleted + runAfterRepaintFinished(tw, () -> { + for (int i = positions.size() - 1; i >= 0; i--) { + Position pos = positions.get(i); + String replaceStr = replaceStrings.get(i); + try { + tv.getDocument().replace(pos.offset, pos.length, replaceStr); + } catch (BadLocationException e) { + error(e); + } + } + }); + }); + } + addToolbarAction(tm, "Cancel", () -> { + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff == null) { + return; + } + var anno = (Annotation) toolbarShellForOneDiff.getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + UnifiedDiff diff = getUnifiedDiffForAnno(anno); + int idx = diff.container.indexOf(diff); + if (idx < 0) { + throw new IllegalStateException("UnifiedDiff not found in container"); //$NON-NLS-1$ + } + idx++; + if (idx >= diff.container.size()) { + idx = 0; + } + UnifiedDiff next = diff.container.get(idx); + if (next == diff) { + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } else { + List nextAnnos = getAllAnnotationsForUnifiedDiff(model, next); + if (nextAnnos.size() > 0) { + disposeToolbarForOneDiff(toolbarShellForOneDiff, tv); + Position pos = getMinPosition(model, nextAnnos); + tv.revealRange(pos.offset, pos.length); + tv.setSelectedRange(pos.offset, 0); + // we can update the toolbar location after the line header code minings are + // removed + runAfterRepaintFinished(tw, () -> setToolbarLocationForOneDiff(tv, model, nextAnnos.get(0))); + } + } + diff.container.remove(diff); + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + for (var lanno : annos) { + model.removeAnnotation(lanno); + } + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + }); + } else { + addToolbarAction(tm, "Keep", () -> { + Shell toolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (toolbarShellForOneDiff == null) { + return; + } + var anno = (Annotation) toolbarShellForOneDiff.getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + UnifiedDiff diff = getUnifiedDiffForAnno(anno); + int idx = diff.container.indexOf(diff); + if (idx < 0) { + throw new IllegalStateException("UnifiedDiff not found in container"); //$NON-NLS-1$ + } + idx++; + if (idx >= diff.container.size()) { + idx = 0; + } + UnifiedDiff next = diff.container.get(idx); + if (next == diff) { + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } else { + List nextAnnos = getAllAnnotationsForUnifiedDiff(model, next); + if (nextAnnos.size() > 0) { + disposeToolbarForOneDiff(toolbarShellForOneDiff, tv); + Position pos = getMinPosition(model, nextAnnos); + tv.revealRange(pos.offset, pos.length); + tv.setSelectedRange(pos.offset, 0); + // we can update the toolbar location after the line header code minings are + // removed + runAfterRepaintFinished(tw, () -> setToolbarLocationForOneDiff(tv, model, nextAnnos.get(0))); + } + } + diff.container.remove(diff); + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + for (var lanno : annos) { + model.removeAnnotation(lanno); + } + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + }); + addToolbarAction(tm, "Undo", () -> { + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff == null) { + return; + } + var anno = (Annotation) fToolbarShellForOneDiff.getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + UnifiedDiff diff = getUnifiedDiffForAnno(anno); + int idx = diff.container.indexOf(diff); + if (idx < 0) { + return; + } + idx++; + if (idx >= diff.container.size()) { + idx = 0; + } + UnifiedDiff next = diff.container.get(idx); + final Annotation[] nextAnno = new Annotation[] { null }; + if (next == diff) { + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } else { + nextAnno[0] = getFirstAnnotationForUnifiedDiff(model, next); + } + diff.container.remove(diff); + List annos = getAllAnnotationsForUnifiedDiff(model, diff); + int unifiedDiffAdditionCount = 0; + for (var lanno : annos) { + if (lanno instanceof UnifiedDiffAnnotation) { + unifiedDiffAdditionCount++; + if (unifiedDiffAdditionCount > 1) { + throw new IllegalStateException( + "Multiple UnifiedDiffAdditionAnnotation for one UnifiedDiff found"); //$NON-NLS-1$ + } + Position pos = model.getPosition(lanno); + // we have to insert with delay because otherwise the line header code minings + // cannot be deleted + runAfterRepaintFinished(tw, () -> { + try { + tv.getDocument().replace(pos.offset, pos.length, diff.leftStr); + if (nextAnno[0] != null) { + disposeToolbarForOneDiff(fToolbarShellForOneDiff, tv); + Position pos1 = model.getPosition(nextAnno[0]); + tv.revealRange(pos1.offset, pos1.length); + tv.setSelectedRange(pos1.offset, 0); + setToolbarLocationForOneDiff(tv, model, nextAnno[0]); + } + } catch (BadLocationException e) { + error(e); + } + }); + } + model.removeAnnotation(lanno); + } + if (unifiedDiffAdditionCount == 0) { + throw new IllegalStateException("No UnifiedDiffAdditionAnnotation for UnifiedDiff found"); //$NON-NLS-1$ + } + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + }); + } + if (tm.isEmpty()) { + return; + } + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff != null && !fToolbarShellForOneDiff.isDisposed()) { + disposeToolbarForOneDiff(fToolbarShellForOneDiff, tv); + } + fToolbarShellForOneDiff = new Shell(parentShell, shellStyle); + tw.setData(TOOLBAR_SHELL_FOR_ONE_DIFF_KEY, fToolbarShellForOneDiff); + var layout = new GridLayout(1, false); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 0; + fToolbarShellForOneDiff.setLayout(layout); + + var fContentComposite = new Composite(fToolbarShellForOneDiff, SWT.NONE); + fContentComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + fContentComposite.setLayout(new FillLayout()); + + ToolBar fToolBar = tm.createControl(fContentComposite); + GridData gd = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, false); + fToolBar.setLayoutData(gd); + + fToolbarShellForOneDiff.pack(); + ScrollBar verticalBar = tw.getVerticalBar(); + if (verticalBar != null) { + Stream stream = verticalBar.getTypedListeners(SWT.Selection, + VerticalBarSelectionAdapter.class); + if (stream == null || stream.count() == 0) { + verticalBar.addSelectionListener(new VerticalBarSelectionAdapter(tv, model)); + } + } + Stream stream = tw.getTypedListeners(SWT.Dispose, + ToolbarDisposeListenerForOneAndAllDiffs.class); + if (stream == null || stream.count() == 0) { + tw.addDisposeListener(new ToolbarDisposeListenerForOneAndAllDiffs(tv, model)); + } + } + + private static final class VerticalBarSelectionAdapter extends SelectionAdapter { + private final IAnnotationModel model; + private final ITextViewer tv; + + public VerticalBarSelectionAdapter(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + @Override + public void widgetSelected(SelectionEvent e) { + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff == null || fToolbarShellForOneDiff.isDisposed()) { + return; + } + var anno = (Annotation) fToolbarShellForOneDiff.getData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY); + if (anno != null) { + setToolbarLocationForOneDiff(this.tv, this.model, anno); + } + } + } + + private static final class ToolbarDisposeListenerForOneAndAllDiffs implements DisposeListener { + + private final ITextViewer tv; + private final IAnnotationModel model; + + public ToolbarDisposeListenerForOneAndAllDiffs(ITextViewer tv, IAnnotationModel model) { + this.tv = tv; + this.model = model; + } + + @Override + public void widgetDisposed(DisposeEvent e) { + disposeUnifiedDiff(this.tv, this.model, (StyledText) e.getSource()); + } + } + + private static UnifiedDiff getUnifiedDiffForAnno(Annotation anno) { + if (anno instanceof DetailedDiffAnnotation da) { + return da.getUnifiedDiff(); + } else if (anno instanceof UnifiedDiffAnnotation a) { + return a.getUnifiedDiff(); + } else if (anno instanceof CodeMiningLineHeaderAnnotation h) { + try { + // TODO (tm) eclipse API needed to access the minings + Field f = h.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(h); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffLineHeaderCodeMining idlhcm) { + return idlhcm.getUnifiedDiff(); + } + } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) { + error(ex); + } + } else if (anno instanceof CodeMiningDocumentFooterAnnotation footer) { + try { + // TODO (tm) eclipse API needed to access the minings + Field f = footer.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(footer); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffFooterCodeMining idlhcm) { + return idlhcm.getUnifiedDiff(); + } + } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) { + error(ex); + } + } + return null; + } + + private static void disposeToolbarForOneDiff(Shell fToolbarShellForOneDiff, ITextViewer tv) { + if (fToolbarShellForOneDiff != null) { + fToolbarShellForOneDiff.dispose(); + } + if (tv == null) { + return; + } + var tw = tv.getTextWidget(); + if (tw == null) { + return; + } + tw.setData(TOOLBAR_SHELL_FOR_ONE_DIFF_KEY, null); + } + + private static void disposeToolbarForAllDiffs(Shell toolbarShellForAllDiffs, ITextViewer tv) { + if (toolbarShellForAllDiffs != null) { + toolbarShellForAllDiffs.dispose(); + } + if (tv == null) { + return; + } + var tw = tv.getTextWidget(); + if (tw == null) { + return; + } + tw.setData(TOOLBAR_SHELL_FOR_ALL_DIFFS_KEY, null); + } + + private static void setToolbarLocationForOneDiff(ITextViewer tv, IAnnotationModel model, Annotation anno) { + var fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff == null || fToolbarShellForOneDiff.isDisposed()) { + drawToolbarForOneDiff(tv, model); + fToolbarShellForOneDiff = getToolbarShellForOneDiff(tv.getTextWidget()); + if (fToolbarShellForOneDiff == null) { + return; + } + } + UnifiedDiff unifiedDiff = getUnifiedDiffForAnno(anno); + if (unifiedDiff == null) { + return; + } + List all = getAllAnnotationsForUnifiedDiff(model, unifiedDiff); + if (all.size() == 0) { + return; + } + Position pos = getMinPosition(model, all); + fToolbarShellForOneDiff.setData(CURRENT_SELECTED_UNIFIED_DIFF_ANNO_KEY, anno); + StyledText tw = tv.getTextWidget(); + Rectangle clientArea = tw.getClientArea(); + + int startOffset = pos.offset; + if (tv instanceof ProjectionViewer pv) { + startOffset = pv.modelOffset2WidgetOffset(pos.offset); + if (startOffset < 0) { + return; // not visible + } + } + if (startOffset >= tw.getCharCount()) { + // ensure no out of bounds + startOffset = tw.getCharCount() - 1; + } + Rectangle startBounds = tw.getTextBounds(startOffset, startOffset); + if (!startBounds.intersects(clientArea)) { + fToolbarShellForOneDiff.setVisible(false); + return; + } + Geometry.moveInside(startBounds, clientArea); + Rectangle displayRelativeBounds = Geometry.toDisplay(tw, startBounds); + + Rectangle twBounds = tw.getBounds(); + Geometry.moveInside(twBounds, clientArea); + Rectangle relativeTwBounds = Geometry.toDisplay(tw, twBounds); + fToolbarShellForOneDiff.setLocation( + relativeTwBounds.x + relativeTwBounds.width - fToolbarShellForOneDiff.getBounds().width, + displayRelativeBounds.y); + fToolbarShellForOneDiff.setVisible(true); + } + + private static Position getMinPosition(IAnnotationModel model, List all) { + Position min = null; + for (Annotation ann : all) { + Position position = model.getPosition(ann); + if (min == null) { + min = position; + } else if (position.offset < min.offset) { + min = position; + } + } + return min; + } + + static Annotation getMinPositionAnno(IAnnotationModel model, List all) { + Position min = null; + Annotation result = null; + for (Annotation ann : all) { + Position position = model.getPosition(ann); + if (min == null) { + min = position; + result = ann; + } else if (position.offset < min.offset) { + min = position; + result = ann; + } + } + return result; + } + + private static void addToolbarAction(ToolBarManager tm, String text, Runnable runnable) { + String tooltip = text; + var action = new Action(text, SWT.PUSH) { // $NON-NLS-1$ + @Override + public void run() { + setChecked(false); + runnable.run(); + } + }; + action.setToolTipText(tooltip); + var actionItem = new ActionContributionItem(action); + actionItem.setMode(ActionContributionItem.MODE_FORCE_TEXT); + tm.add(actionItem); + tm.add(new Separator()); + } + + private static void addToolbarAction(ToolBarManager tm, Action action) { + var a = new Action(action.getText(), SWT.PUSH) { // $NON-NLS-1$ + @Override + public void run() { + setChecked(false); + action.run(); + } + }; + action.setToolTipText(action.getToolTipText()); + action.setImageDescriptor(action.getImageDescriptor()); + var actionItem = new ActionContributionItem(a); + actionItem.setMode(ActionContributionItem.MODE_FORCE_TEXT); + tm.add(actionItem); + tm.add(new Separator()); + } + + private static void addMouseMoveListener(ITextViewer tv, IAnnotationModel model) { + StyledText textWidget = tv.getTextWidget(); + Stream stream = textWidget.getTypedListeners(SWT.MouseMove, + UnifiedDiffMouseMoveListener.class); + if (stream != null && stream.count() > 0) { + return; + } + textWidget.addMouseMoveListener(new UnifiedDiffMouseMoveListener(model, tv)); + } + + private static final class UnifiedDiffMouseMoveListener implements MouseMoveListener { + private final IAnnotationModel model; + private final ITextViewer tv; + private final StyledText textWidget; + + public UnifiedDiffMouseMoveListener(IAnnotationModel model, ITextViewer tv) { + this.model = model; + this.tv = tv; + this.textWidget = tv.getTextWidget(); + } + + @Override + public void mouseMove(MouseEvent e) { + Iterator it = this.model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation anno = getUnifiedDiffAnnotationFromIterator(it); + if (anno == null) { + continue; + } + Position pos = this.model.getPosition(anno); + int startOffset = pos.offset; + if (tv instanceof ProjectionViewer pv) { + startOffset = pv.modelOffset2WidgetOffset(pos.offset); + } + int endOffset = startOffset + pos.length; + try { + Rectangle startBounds = this.textWidget.getTextBounds(startOffset, startOffset); + Rectangle endBounds = this.textWidget.getTextBounds(endOffset, endOffset); + if (startBounds.y == endBounds.y) { + endBounds.y += endBounds.height; + } + if (e.y >= startBounds.y && e.y <= endBounds.y) { + setToolbarLocationForOneDiff(this.tv, this.model, anno); + return; + } + } catch (IllegalArgumentException ex) { // NOPMD silently ignored + } + } + } + } + + private static Annotation getFirstAnnotationForUnifiedDiff(IAnnotationModel model, UnifiedDiff unifiedDiff) { + Iterator it = model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation anno = getUnifiedDiffAnnotationFromIterator(it); + if (anno == null) { + continue; + } + UnifiedDiff unifiedDiffForAnno = getUnifiedDiffForAnno(anno); + if (unifiedDiffForAnno == unifiedDiff) { + return anno; + } + } + return null; + } + + static List getAllAnnotationsForUnifiedDiff(IAnnotationModel model, UnifiedDiff unifiedDiff) { + List result = new ArrayList<>(); + Iterator it = model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation anno = getUnifiedDiffAnnotationFromIterator(it); + if (anno == null) { + continue; + } + UnifiedDiff unifiedDiffForAnno = getUnifiedDiffForAnno(anno); + if (unifiedDiffForAnno == unifiedDiff) { + result.add(anno); + } + } + return result; + } + + private static Annotation getUnifiedDiffAnnotationFromIterator(Iterator it) { + boolean doit = false; + Annotation anno = it.next(); + if (anno instanceof CodeMiningLineHeaderAnnotation h) { + try { + // TODO (tm) eclipse API needed to access the minings + Field f = h.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(h); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffLineHeaderCodeMining) { + doit = true; + } + } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) { + error(ex); + } + } else if (anno instanceof CodeMiningDocumentFooterAnnotation footer) { + try { + // TODO (tm) eclipse API needed to access the minings + Field f = footer.getClass().getDeclaredField("fMinings"); //$NON-NLS-1$ + f.setAccessible(true); + @SuppressWarnings("unchecked") + List m = (List) f.get(footer); + if (m != null && m.size() == 1 && m.get(0) instanceof UnifiedDiffFooterCodeMining idlhcm) { + doit = true; + } + } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) { + error(ex); + } + } else if (ADDITION_ANNO_TYPE.equals(anno.getType()) || DELETION_ANNO_TYPE.equals(anno.getType()) + || DETAILED_ADDITION_ANNO_TYPE.equals(anno.getType()) + || DETAILED_DELETION_ANNO_TYPE.equals(anno.getType())) { + doit = true; + } + if (!doit) { + return null; + } + return anno; + } + + private static void addUndoListener(ITextViewer tv, IDocument document, IAnnotationModel model) { + IDocumentUndoManager documentUndoManager = DocumentUndoManagerRegistry.getDocumentUndoManager(document); + var undoListener = (IDocumentUndoListener) event -> { + UnifiedDiff toBeDeletedDiff = null; + Iterator it = model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation anno = getUnifiedDiffAnnotationFromIterator(it); + if (anno == null) { + continue; + } + Position pos = model.getPosition(anno); + if (pos.offset == event.getOffset()) { + UnifiedDiff diff = getUnifiedDiffForAnno(anno); + if (diff != null && diff.rightStr.equals(event.getPreservedText())) { + toBeDeletedDiff = diff; + break; + } + } + } + if (toBeDeletedDiff == null) { + return; + } + // delete the uni diff + List all = getAllAnnotationsForUnifiedDiff(model, toBeDeletedDiff); + for (var lanno : all) { + model.removeAnnotation(lanno); + } + toBeDeletedDiff.container.remove(toBeDeletedDiff); + if (tv instanceof ISourceViewerExtension5 ext) { + ext.updateCodeMinings(); + } + if (toBeDeletedDiff.container.isEmpty()) { + disposeUnifiedDiff(tv, model, tv.getTextWidget()); + } + }; + tv.getTextWidget().setData(UNDO_LISTENER_KEY, undoListener); + documentUndoManager.addDocumentUndoListener(undoListener); + } + + static void disposeUnifiedDiff(ITextViewer tv, IAnnotationModel model, StyledText tw) { + disposeToolbarForOneDiff(getToolbarShellForOneDiff(tw), tv); + disposeToolbarForAllDiffs(getToolbarShellForAllDiffs(tw), tv); + var undoListener = (IDocumentUndoListener) tw.getData(UNDO_LISTENER_KEY); + if (undoListener != null) { + tw.setData(UNDO_LISTENER_KEY, null); + IDocument document = tv.getDocument(); + if (document != null) { + IDocumentUndoManager documentUndoManager = DocumentUndoManagerRegistry.getDocumentUndoManager(document); + documentUndoManager.removeDocumentUndoListener(undoListener); + } + } + var listener = (UnifiedDiffAnnotationmodelListener) tw.getData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY); + if (listener != null) { + tw.setData(UNIFIED_DIFF_ANNOTATION_MODEL_LISTENER_KEY, null); + model.removeAnnotationModelListener(listener); + } + + Stream stream = tw.getTypedListeners(SWT.FocusIn, UnifiedDiffFocusListener.class); + if (stream != null) { + stream.forEach(l -> tw.removeFocusListener(l)); + } + + diffsByViewer.remove(tv); + + // TODO (tm) remove mouse move listener + // TODO (tm) remove paint listener + } + + private static void addPaintListener(ITextViewer viewer, IAnnotationModel model, UnifiedDiffMode mode) { + StyledText w = viewer.getTextWidget(); + Stream stream = w.getTypedListeners(SWT.Paint, UnifiedDiffPaintListener.class); + stream.forEach(e -> { + w.removePaintListener(e); + }); + w.addPaintListener(new UnifiedDiffPaintListener(viewer, model, mode)); + } + + private static class UnifiedDiffPaintListener implements PaintListener { + + private final IAnnotationModel model; + private final ITextViewer viewer; + private final Color additionBackgroundColor; + private final StyledText w; + + public UnifiedDiffPaintListener(ITextViewer viewer, IAnnotationModel model, UnifiedDiffMode mode) { + this.model = model; + this.viewer = viewer; + w = viewer.getTextWidget(); + RGB color; + if (mode.equals(UnifiedDiffMode.OVERLAY_MODE) || mode.equals(UnifiedDiffMode.OVERLAY_READ_ONLY_MODE)) { + color = JFaceResources.getColorRegistry().getRGB("DELETION_COLOR"); //$NON-NLS-1$ + } else { + color = JFaceResources.getColorRegistry().getRGB("ADDITION_COLOR"); //$NON-NLS-1$ + } + + RGB background = Display.getDefault().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(); + RGB interpolated = UnifiedDiffCodeMiningProvider.interpolate(color, background, 0.9); + this.additionBackgroundColor = new Color(interpolated); + } + + @Override + public void paintControl(PaintEvent e) { + Rectangle bounds = this.w.getBounds(); + Iterator it = this.model.getAnnotationIterator(); + while (it.hasNext()) { + Annotation anno = it.next(); + if (!(ADDITION_ANNO_TYPE.equals(anno.getType()) || DELETION_ANNO_TYPE.equals(anno.getType()))) { + continue; + } + // Fill the area between the last character of the line and the widget boundary. + // This is not done via the Annotations. + int posOffset; + int posLength; + { + Position pos = this.model.getPosition(anno); + if (viewer instanceof ProjectionViewer pv) { + posOffset = pv.modelOffset2WidgetOffset(pos.offset); + if (posOffset < 0) { + continue; // not visible + } + } else { + posOffset = pos.offset; + } + posLength = pos.length; + } + int fromLine = this.w.getLineAtOffset(posOffset); + int toLine = this.w.getLineAtOffset(posOffset + posLength); + e.gc.setBackground(this.additionBackgroundColor); + for (int lineNr = fromLine; lineNr < toLine; lineNr++) { + String line = this.w.getLine(lineNr); + int endLineOffset = this.w.getOffsetAtLine(lineNr) + line.length(); + Rectangle endLineBounds = this.w.getTextBounds(endLineOffset, endLineOffset); + endLineBounds.height += this.w.getLineSpacing(); + if (line.length() == 0) { + // this.w.getTextBounds seems not to work for empty lines containing only \n in + // a document with \r\n as line delimeter + int nextLineOffset = this.w.getOffsetAtLine(lineNr + 1); + Rectangle nextLineBounds = this.w.getTextBounds(nextLineOffset, nextLineOffset); + if (endLineBounds.y + endLineBounds.height < nextLineBounds.y) { + endLineBounds.height = nextLineBounds.y - endLineBounds.y; + } + } + e.gc.fillRectangle(endLineBounds.x + endLineBounds.width, endLineBounds.y, + bounds.width - (endLineBounds.x + endLineBounds.width), endLineBounds.height); + + // annotation painter is not taking line spacing into account - we therefore + // 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()); + } + } + } + } + + private static boolean validateEdit(IFile file) { + final boolean result[] = new boolean[] { false }; + try { + final IFile fFile = file; + ResourcesPlugin.getWorkspace().run((IWorkspaceRunnable) monitor -> { + IStatus status = ResourcesPlugin.getWorkspace().validateEdit(new IFile[] { fFile }, null); + if (status != null && status.isOK()) { + result[0] = true; + } + }, new NullProgressMonitor()); + } catch (CoreException e) { + error(e); + } + return result[0]; + } + + private static ITokenComparator createTokenComparator(String line, TokenComparatorFactory tokenComparatorFactory) { + ITokenComparator result = null; + if (tokenComparatorFactory != null) { + result = tokenComparatorFactory.apply(line); + } + if (result == null) { + result = new TokenComparator(line); + } + return result; + } + + private static int getTokenEnd(ITokenComparator tc, int start, int length) { + return tc.getTokenStart(start + length); + } + + static void selectAndRevealAnno(ITextViewer tv, IAnnotationModel model, Annotation nextAnno) { + disposeToolbarForOneDiff(getToolbarShellForOneDiff(tv.getTextWidget()), tv); + Position pos = model.getPosition(nextAnno); + tv.revealRange(pos.offset, pos.length); + tv.setSelectedRange(pos.offset, 0); + Display.getDefault().asyncExec(() -> setToolbarLocationForOneDiff(tv, model, nextAnno)); + } + + static void runAfterRepaintFinished(StyledText tw, Runnable runnable) { + tw.addPaintListener(new PaintListener() { + private final PaintListener me = this; + private final Runnable r = () -> { + tw.removePaintListener(me); + runnable.run(); + }; + + @Override + public void paintControl(PaintEvent e) { + Display.getDefault().timerExec(100, this.r); + } + }); + } + + public static class UnifiedDiff { + public IDocument left; + public int leftStart; + int leftLength; + public final String leftStr; + + IDocument right; + int rightStart; + int rightLength; + String rightStr; + public final List detailedDiffs = new ArrayList<>(); + final List container; + public final UnifiedDiffMode mode; + + public UnifiedDiff(IDocument left, int leftStart, int leftEnd, String leftDiffSource, IDocument right, + int rightStart, int rightEnd, String rightDiffSource, List container, + UnifiedDiffMode mode) { + this.left = left; + this.leftStart = leftStart; + this.leftLength = leftEnd - leftStart; + this.leftStr = leftDiffSource; + this.right = right; + this.rightStart = rightStart; + this.rightLength = rightEnd - rightStart; + this.rightStr = rightDiffSource; + this.container = container; + this.mode = mode; + } + } +} diff --git a/team/bundles/org.eclipse.compare/plugin.properties b/team/bundles/org.eclipse.compare/plugin.properties index c39f90e8739..da4c613cacc 100644 --- a/team/bundles/org.eclipse.compare/plugin.properties +++ b/team/bundles/org.eclipse.compare/plugin.properties @@ -125,6 +125,7 @@ ComparePreferencePage.generalTab.label= &General ComparePreferencePage.structureCompare.label= &Open structure compare automatically ComparePreferencePage.structureOutline.label= Show structure compare in Outline &view when possible ComparePreferencePage.ignoreWhitespace.label= Ignore &white space +ComparePreferencePage.unifiedDiff.label=Use Unified Diff instead of 2-way compare when possible ComparePreferencePage.saveBeforePatching.label= A&utomatically save dirty editors before browsing patches ComparePreferencePage.regex.description=Enter regular expressions used to identify added or removed lines in a patch\n(e.g. '^\\+\\s*\\S' for an added line with at least one word character). diff --git a/team/bundles/org.eclipse.compare/plugin.xml b/team/bundles/org.eclipse.compare/plugin.xml index 24978d0b081..158902e0187 100644 --- a/team/bundles/org.eclipse.compare/plugin.xml +++ b/team/bundles/org.eclipse.compare/plugin.xml @@ -451,5 +451,150 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +