blob: af1093d5f42676cdb5d6f56634b02b00c57d8e12 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.compare.structuremergeviewer;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.CompareViewerSwitchingPane;
import org.eclipse.compare.ICompareFilter;
import org.eclipse.compare.IContentChangeListener;
import org.eclipse.compare.IContentChangeNotifier;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IDocumentRange;
import org.eclipse.compare.internal.ChangeCompareFilterPropertyAction;
import org.eclipse.compare.internal.CompareMessages;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.services.IDisposable;
/**
* A diff tree viewer that can be configured with a <code>IStructureCreator</code>
* to retrieve a hierarchical structure from the input object (an <code>ICompareInput</code>)
* and perform a two-way or three-way compare on it.
* <p>
* This class may be instantiated; it is not intended to be subclassed outside
* this package.
* </p>
*
* @see IStructureCreator
* @see ICompareInput
* @noextend This class is not intended to be subclassed by clients.
*/
public class StructureDiffViewer extends DiffTreeViewer {
private Differencer fDifferencer;
private boolean fThreeWay= false;
private StructureInfo fAncestorStructure = new StructureInfo();
private StructureInfo fLeftStructure = new StructureInfo();
private StructureInfo fRightStructure = new StructureInfo();
private IStructureCreator fStructureCreator;
private IDiffContainer fRoot;
private IContentChangeListener fContentChangedListener;
private CompareViewerSwitchingPane fParent;
private ICompareInputChangeListener fCompareInputChangeListener;
/*
* A set of background tasks for updating the structure
*/
private IRunnableWithProgress diffTask = monitor -> {
monitor.beginTask(CompareMessages.StructureDiffViewer_0, 100);
diff(SubMonitor.convert(monitor, 100));
monitor.done();
};
private IRunnableWithProgress inputChangedTask = monitor -> {
monitor.beginTask(CompareMessages.StructureDiffViewer_1, 100);
// TODO: Should we always force
compareInputChanged((ICompareInput) getInput(), true, SubMonitor.convert(monitor, 100));
monitor.done();
};
/*
* A helper class for holding the input and generated structure
* for the ancestor, left and right inputs.
*/
private class StructureInfo {
private ITypedElement fInput;
private IStructureComparator fStructureComparator;
private IRunnableWithProgress refreshTask = monitor -> refresh(monitor);
public boolean setInput(ITypedElement newInput, boolean force, IProgressMonitor monitor) {
boolean changed = false;
if (force || newInput != fInput) {
removeDocumentRangeUpdaters();
if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
((IContentChangeNotifier)fInput).removeContentChangeListener(fContentChangedListener);
fInput= newInput;
if (fInput == null) {
dispose(); // destroy fStructureComparator
fStructureComparator= null;
} else {
refresh(monitor);
changed= true;
}
if (fInput instanceof IContentChangeNotifier && fContentChangedListener != null)
((IContentChangeNotifier)fInput).addContentChangeListener(fContentChangedListener);
}
return changed;
}
/**
* Remove any document range updaters that were registered against the document.
*/
private void removeDocumentRangeUpdaters() {
if (fStructureComparator instanceof IDocumentRange) {
IDocument doc = ((IDocumentRange) fStructureComparator).getDocument();
try {
doc.removePositionCategory(IDocumentRange.RANGE_CATEGORY);
} catch (BadPositionCategoryException ex) {
// Ignore
}
}
}
public IStructureComparator getStructureComparator() {
return fStructureComparator;
}
public void refresh(IProgressMonitor monitor) {
IStructureComparator oldComparator = fStructureComparator;
fStructureComparator= createStructure(monitor);
// Dispose of the old one after in case they are using a shared document
// (i.e. disposing it after will hold on to a reference to the document
// so it doesn't get freed and reloaded)
if (oldComparator instanceof IDisposable) {
IDisposable disposable = (IDisposable) oldComparator;
disposable.dispose();
}
}
public Object getInput() {
return fInput;
}
private IStructureComparator createStructure(IProgressMonitor monitor) {
// Defend against concurrent disposal
Object input = fInput;
if (input == null)
return null;
if (fStructureCreator instanceof IStructureCreator2) {
IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
try {
return sc2.createStructure(input, monitor);
} catch (CoreException e) {
CompareUIPlugin.log(e);
}
}
return fStructureCreator.getStructure(input);
}
public void dispose() {
if (fStructureComparator != null && fStructureCreator instanceof IStructureCreator2) {
IStructureCreator2 sc2 = (IStructureCreator2) fStructureCreator;
sc2.destroy(fStructureComparator);
}
}
public IRunnableWithProgress getRefreshTask() {
return refreshTask;
}
}
/**
* Creates a new viewer for the given SWT tree control with the specified configuration.
*
* @param tree the tree control
* @param configuration the configuration for this viewer
*/
public StructureDiffViewer(Tree tree, CompareConfiguration configuration) {
super(tree, configuration);
Composite c= tree.getParent();
if (c instanceof CompareViewerSwitchingPane)
fParent= (CompareViewerSwitchingPane) c;
initialize();
}
/**
* Creates a new viewer under the given SWT parent with the specified configuration.
*
* @param parent the SWT control under which to create the viewer
* @param configuration the configuration for this viewer
*/
public StructureDiffViewer(Composite parent, CompareConfiguration configuration) {
super(parent, configuration);
if (parent instanceof CompareViewerSwitchingPane)
fParent= (CompareViewerSwitchingPane) parent;
initialize();
}
private void initialize() {
setAutoExpandLevel(3);
fContentChangedListener= changed -> StructureDiffViewer.this.contentChanged(changed);
fCompareInputChangeListener = input -> StructureDiffViewer.this.compareInputChanged(input, true);
}
/**
* Configures the <code>StructureDiffViewer</code> with a structure creator.
* The structure creator is used to create a hierarchical structure
* for each side of the viewer's input element of type <code>ICompareInput</code>.
*
* @param structureCreator the new structure creator
*/
public void setStructureCreator(IStructureCreator structureCreator) {
if (fStructureCreator != structureCreator) {
fStructureCreator= structureCreator;
Control tree= getControl();
if (tree != null && !tree.isDisposed())
tree.setData(CompareUI.COMPARE_VIEWER_TITLE, getTitle());
}
}
/**
* Returns the structure creator or <code>null</code> if no
* structure creator has been set with <code>setStructureCreator</code>.
*
* @return the structure creator or <code>null</code>
*/
public IStructureCreator getStructureCreator() {
return fStructureCreator;
}
/**
* Reimplemented to get the descriptive title for this viewer from the <code>IStructureCreator</code>.
* @return the viewer's name
*/
@Override
public String getTitle() {
if (fStructureCreator != null)
return fStructureCreator.getName();
return super.getTitle();
}
/**
* Overridden because the input of this viewer is not identical to the root of the tree.
* The tree's root is a IDiffContainer that was returned from the method <code>diff</code>.
*
* @return the root of the diff tree produced by method <code>diff</code>
*/
@Override
protected Object getRoot() {
return fRoot;
}
/*
* Method declared on StructuredViewer. Overridden to create the comparable
* structures from the input object and to feed them through the differencing
* engine. Note: for this viewer the value from <code>getInput</code> is not
* identical to <code>getRoot</code>.
*/
@Override
protected void inputChanged(Object input, Object oldInput) {
if (oldInput instanceof ICompareInput) {
ICompareInput old = (ICompareInput) oldInput;
old.removeCompareInputChangeListener(fCompareInputChangeListener);
}
if (input instanceof ICompareInput) {
ICompareInput ci = (ICompareInput) input;
ci.addCompareInputChangeListener(fCompareInputChangeListener);
compareInputChanged(ci);
if (input != oldInput)
initialSelection();
}
}
@Override
protected void initialSelection() {
expandToLevel(2);
}
/* (non Javadoc)
* Overridden to unregister all listeners.
*/
@Override
protected void handleDispose(DisposeEvent event) {
Object input = getInput();
if (input instanceof ICompareInput) {
ICompareInput ci = (ICompareInput) input;
ci.removeCompareInputChangeListener(fCompareInputChangeListener);
}
compareInputChanged(null);
fContentChangedListener= null;
super.handleDispose(event);
}
/**
* Recreates the comparable structures for the input sides.
* @param input this viewer's new input
*/
protected void compareInputChanged(ICompareInput input) {
compareInputChanged(input, false);
}
/* package */ void compareInputChanged(final ICompareInput input, final boolean force) {
if (input == null) {
// When closing, we don't need a progress monitor to handle the input change
compareInputChanged(input, force, null);
return;
}
CompareConfiguration cc = getCompareConfiguration();
// The compare configuration is nulled when the viewer is disposed
if (cc != null) {
BusyIndicator.showWhile(Display.getDefault(), () -> {
try {
inputChangedTask.run(new NullProgressMonitor());
} catch (InvocationTargetException e1) {
CompareUIPlugin.log(e1.getTargetException());
} catch (InterruptedException e2) {
// Ignore
}
});
}
}
/* package */ void compareInputChanged(ICompareInput input, boolean force, IProgressMonitor monitor) {
ITypedElement t= null;
boolean changed= false;
if (input != null)
t= input.getAncestor();
fThreeWay= (t != null);
beginWork(monitor, 400);
try {
if (fAncestorStructure.setInput(t, force, subMonitor(monitor, 100)))
changed = true;
if (input != null)
t= input.getLeft();
if (fLeftStructure.setInput(t, force, subMonitor(monitor, 100)))
changed = true;
if (input != null)
t= input.getRight();
if (fRightStructure.setInput(t, force, subMonitor(monitor, 100)))
changed = true;
// The compare configuration is nulled when the viewer is disposed
CompareConfiguration cc = getCompareConfiguration();
if (changed && cc != null)
cc.getContainer().runAsynchronously(diffTask);
} finally {
endWork(monitor);
}
}
private void endWork(IProgressMonitor monitor) {
if (monitor != null)
monitor.done();
}
private IProgressMonitor subMonitor(IProgressMonitor monitor, int work) {
if (monitor != null) {
if (monitor.isCanceled() || getControl().isDisposed())
throw new OperationCanceledException();
return SubMonitor.convert(monitor, work);
}
return null;
}
private void beginWork(IProgressMonitor monitor, int totalWork) {
if (monitor != null)
monitor.beginTask(null, totalWork);
}
/**
* Calls <code>diff</code> whenever the byte contents changes.
* @param changed the object that sent out the notification
*/
protected void contentChanged(final IContentChangeNotifier changed) {
if (fStructureCreator == null)
return;
if (changed == null) {
getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
} else if (changed == fAncestorStructure.getInput()) {
getCompareConfiguration().getContainer().runAsynchronously(fAncestorStructure.getRefreshTask());
} else if (changed == fLeftStructure.getInput()) {
getCompareConfiguration().getContainer().runAsynchronously(fLeftStructure.getRefreshTask());
} else if (changed == fRightStructure.getInput()) {
getCompareConfiguration().getContainer().runAsynchronously(fRightStructure.getRefreshTask());
} else {
return;
}
getCompareConfiguration().getContainer().runAsynchronously(diffTask);
}
/**
* This method is called from within <code>diff()</code> before the
* difference tree is being built. Clients may override this method to
* perform their own pre-processing. This default implementation does
* nothing.
*
* @param ancestor the ancestor input to the differencing operation
* @param left the left input to the differencing operation
* @param right the right input to the differencing operation
* @since 2.0
* @deprecated Clients should override
* {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator, IProgressMonitor)}
*/
@Deprecated
protected void preDiffHook(IStructureComparator ancestor, IStructureComparator left, IStructureComparator right) {
// we do nothing here
}
/**
* This method is called from within {@link #diff(IProgressMonitor)} before
* the difference tree is being built. This method may be called from a
* background (non-UI) thread).
* <p>
* For backwards compatibility, this default implementation calls
* {@link #preDiffHook(IStructureComparator, IStructureComparator, IStructureComparator)}
* from the UI thread. Clients should override this method even if they
* don't perform pre-processing to avoid the call to the UI thread.
*
* @param ancestor the ancestor input to the differencing operation
* @param left the left input to the differencing operation
* @param right the right input to the differencing operation
* @param monitor a progress monitor or null if progress is not required
* @since 3.3
*/
protected void preDiffHook(final IStructureComparator ancestor, final IStructureComparator left, final IStructureComparator right, IProgressMonitor monitor) {
syncExec(() -> preDiffHook(ancestor, left, right));
}
/**
* Runs the difference engine and refreshes the tree. This method may be called
* from a background (non-UI) thread).
* @param monitor a progress monitor or <code>null</code> if progress in not required
*/
protected void diff(IProgressMonitor monitor) {
try {
beginWork(monitor, 150);
IStructureComparator ancestorComparator = fAncestorStructure.getStructureComparator();
IStructureComparator leftComparator = fLeftStructure.getStructureComparator();
IStructureComparator rightComparator = fRightStructure.getStructureComparator();
preDiffHook(ancestorComparator,
leftComparator,
rightComparator,
subMonitor(monitor, 25));
String message= null;
if ((fThreeWay && ancestorComparator == null) || leftComparator == null || rightComparator == null) {
// could not get structure of one (or more) of the legs
fRoot= null;
message= CompareMessages.StructureDiffViewer_StructureError;
} else { // calculate difference of the two (or three) structures
if (fDifferencer == null)
fDifferencer= new Differencer() {
@Override
protected boolean contentsEqual(Object o1,
char contributor1, Object o2, char contributor2) {
return StructureDiffViewer.this.contentsEqual(o1,
contributor1, o2, contributor2);
}
@Override
protected Object visit(Object data, int result, Object ancestor, Object left, Object right) {
Object o= super.visit(data, result, ancestor, left, right);
if (!getCompareConfiguration().isMirrored() && o instanceof DiffNode)
((DiffNode)o).swapSides(true);
return o;
}
};
fRoot= (IDiffContainer) fDifferencer.findDifferences(fThreeWay, subMonitor(monitor, 100), null,
ancestorComparator, leftComparator, rightComparator);
if (fRoot == null || fRoot.getChildren().length == 0) {
message= CompareMessages.StructureDiffViewer_NoStructuralDifferences;
} else {
postDiffHook(fDifferencer, fRoot, subMonitor(monitor, 25));
}
}
if (Display.getCurrent() != null) {
refreshAfterDiff(message);
} else {
final String theMessage = message;
Display.getDefault().asyncExec(() -> refreshAfterDiff(theMessage));
}
} finally {
endWork(monitor);
}
}
private void refreshAfterDiff(String message) {
if (getControl().isDisposed())
return;
if (fParent != null)
fParent.setTitleArgument(message);
refresh(getRoot());
// Setting the auto-expand level doesn't do anything for refreshes
expandToLevel(3);
}
/**
* Runs the difference engine and refreshes the tree.
*/
protected void diff() {
try {
CompareConfiguration compareConfiguration = getCompareConfiguration();
// A null compare configuration indicates that the viewer was disposed
if (compareConfiguration != null) {
compareConfiguration.getContainer().run(true, true, monitor -> {
monitor.beginTask(CompareMessages.StructureDiffViewer_2, 100);
diffTask.run(SubMonitor.convert(monitor, 100));
monitor.done();
});
}
} catch (InvocationTargetException e) {
// Shouldn't happen since the run doesn't throw
CompareUIPlugin.log(e.getTargetException());
handleFailedRefresh(e.getTargetException().getMessage());
} catch (InterruptedException e) {
// Canceled by user
handleFailedRefresh(CompareMessages.StructureDiffViewer_3);
}
}
private void handleFailedRefresh(final String message) {
Runnable runnable = () -> {
if (getControl().isDisposed())
return;
refreshAfterDiff(message);
};
if (Display.getCurrent() != null) {
runnable.run();
} else {
Display.getDefault().asyncExec(runnable);
}
}
/**
* This method is called from within <code>diff()</code> after the
* difference tree has been built. Clients may override this method to
* perform their own post-processing. This default implementation does
* nothing.
*
* @param differencer the differencer used to perform the differencing
* @param root the non-<code>null</code> root node of the difference tree
* @since 2.0
* @deprecated Subclasses should override
* {@link #postDiffHook(Differencer, IDiffContainer, IProgressMonitor)}
* instead
*/
@Deprecated
protected void postDiffHook(Differencer differencer, IDiffContainer root) {
// we do nothing here
}
/**
* This method is called from within {@link #diff(IProgressMonitor)} after
* the difference tree has been built. This method may be called from a
* background (non-UI) thread).
* <p>
* For backwards compatibility, this default implementation calls
* {@link #postDiffHook(Differencer, IDiffContainer)} from the UI thread.
* Clients should override this method even if they don't perform post
* processing to avoid the call to the UI thread.
*
* @param differencer the differencer used to perform the differencing
* @param root the non-<code>null</code> root node of the difference tree
* @param monitor a progress monitor or <code>null</code> if progress is
* not required
* @since 3.3
*/
protected void postDiffHook(final Differencer differencer, final IDiffContainer root, IProgressMonitor monitor) {
syncExec(() -> postDiffHook(differencer, root));
}
/*
* Performs a byte compare on the given objects.
* Called from the difference engine.
* Returns <code>null</code> if no structure creator has been set.
*/
private boolean contentsEqual(Object o1, char contributor1, Object o2,
char contributor2) {
if (fStructureCreator != null) {
boolean ignoreWhiteSpace = Utilities.getBoolean(
getCompareConfiguration(),
CompareConfiguration.IGNORE_WHITESPACE, false);
ICompareFilter[] compareFilters =
Utilities.getCompareFilters(getCompareConfiguration());
String s1, s2;
if (compareFilters != null && compareFilters.length > 0
&& fStructureCreator instanceof StructureCreator) {
return ((StructureCreator) fStructureCreator).contentsEquals(
o1, contributor1, o2, contributor2, ignoreWhiteSpace,
compareFilters);
}
s1 = fStructureCreator.getContents(o1, ignoreWhiteSpace);
s2 = fStructureCreator.getContents(o2, ignoreWhiteSpace);
if (s1 == null || s2 == null)
return false;
return s1.equals(s2);
}
return false;
}
/**
* Tracks property changes of the configuration object.
* Clients may override to track their own property changes.
* In this case they must call the inherited method.
* @param event the property changed event that triggered the call to this method
*/
@Override
protected void propertyChange(PropertyChangeEvent event) {
String key= event.getProperty();
if (key.equals(CompareConfiguration.IGNORE_WHITESPACE)) {
diff();
} else if (key.equals(ChangeCompareFilterPropertyAction.COMPARE_FILTERS)
&& getCompareConfiguration().getProperty(
ChangeCompareFilterPropertyAction.COMPARE_FILTERS_INITIALIZING) == null) {
diff();
} else if (key.equals("ANCESTOR_STRUCTURE_REFRESH")) { //$NON-NLS-1$
fAncestorStructure.refresh(new NullProgressMonitor());
diff();
} else if (key.equals("LEFT_STRUCTURE_REFRESH")) { //$NON-NLS-1$
fLeftStructure.refresh(new NullProgressMonitor());
diff();
} else if (key.equals("RIGHT_STRUCTURE_REFRESH")) { //$NON-NLS-1$
fRightStructure.refresh(new NullProgressMonitor());
diff();
} else if (key.equals("ALL_STRUCTURE_REFRESH")) { //$NON-NLS-1$
fAncestorStructure.refresh(new NullProgressMonitor());
fLeftStructure.refresh(new NullProgressMonitor());
fRightStructure.refresh(new NullProgressMonitor());
diff();
} else {
super.propertyChange(event);
}
}
/**
* Overridden to call the <code>save</code> method on the structure creator after
* nodes have been copied from one side to the other side of an input object.
*
* @param leftToRight if <code>true</code> the left side is copied to the right side.
* If <code>false</code> the right side is copied to the left side
*/
@Override
protected void copySelected(boolean leftToRight) {
super.copySelected(leftToRight);
if (fStructureCreator != null) {
fStructureCreator.save(
leftToRight ? fRightStructure.getStructureComparator() : fLeftStructure.getStructureComparator(),
leftToRight ? fRightStructure.getInput() : fLeftStructure.getInput());
}
}
private void syncExec(final Runnable runnable) {
if (getControl().isDisposed())
return;
if (Display.getCurrent() != null) {
runnable.run();
} else {
getControl().getDisplay().syncExec(() -> {
if (!getControl().isDisposed()) runnable.run();
});
}
}
}