blob: ddccccab248d028a7f194e431db342493a9d4ecc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016-2017 Ericsson AB.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Ericsson - initial API and implementation
*******************************************************************************/
package org.eclipse.egerrit.internal.ui.compare;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareViewerPane;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.ContentMergeViewer;
import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.MergeSourceViewer;
import org.eclipse.compare.internal.ViewerDescriptor;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egerrit.internal.core.GerritClient;
import org.eclipse.egerrit.internal.core.exception.EGerritException;
import org.eclipse.egerrit.internal.model.ChangeInfo;
import org.eclipse.egerrit.internal.model.FileInfo;
import org.eclipse.egerrit.internal.model.RevisionInfo;
import org.eclipse.egerrit.internal.model.impl.StringToFileInfoImpl;
import org.eclipse.egerrit.internal.ui.EGerritUIPlugin;
import org.eclipse.egerrit.internal.ui.editors.OpenCompareEditor;
import org.eclipse.egerrit.internal.ui.editors.QueryHelpers;
import org.eclipse.egerrit.internal.ui.utils.Messages;
import org.eclipse.emf.common.util.WeakInterningHashSet;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationPainter;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement;
import org.eclipse.team.ui.synchronize.SaveableCompareEditorInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GerritMultipleInput extends SaveableCompareEditorInput {
private static Logger logger = LoggerFactory.getLogger(GerritMultipleInput.class);
static final String WORKSPACE = "WORKSPACE"; //$NON-NLS-1$
public static final String BASE = "BASE"; //$NON-NLS-1$
private GerritDiffNode root;
private ChangeInfo changeInfo;
private FileInfo fileToReveal; //The file requested by the user
DiffNode nodeToReveal; //The node representing the file we want to reveal
private String leftSide; //workspace, base, revision number
private String rightSide; //workspace, base, revision number
GerritClient gerritClient;
//Flag used to detect when a failure happened while saving the comments to the server.
//It can take 3 values -1 (no problem), 0 problem on the left side, 1 problem on the right side
private byte problemSavingChanges = -1;
private Runnable postSaveListener;
/**
* Define an input for the compare editor. Left side represents what to display in the left side of the editor
* (workspace, revisionId, base) Right side represents what to display in the left side of the editor (workspace,
* revisionId, base)
*/
public GerritMultipleInput(String leftSide, String rightSide, ChangeInfo changeInfo, GerritClient gerrit,
FileInfo toReveal) {
super(initConfiguration(), null);
Assert.isNotNull(leftSide);
Assert.isNotNull(rightSide);
Assert.isNotNull(changeInfo);
this.leftSide = leftSide;
this.rightSide = rightSide;
this.changeInfo = changeInfo;
this.fileToReveal = toReveal;
this.gerritClient = gerrit;
root = new GerritDiffNode(GerritDifferences.NO_CHANGE) {
@Override
public String getName() {
return GerritMultipleInput.this.getName();
}
@Override
public boolean hasChildren() {
return true;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
//Do nothing. This is just here to make sure databinding is not throwing exception
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
//Do nothing. This is just here to make sure databinding is not throwing exception
}
};
}
private static CompareConfiguration initConfiguration() {
CompareConfiguration config = new CompareConfiguration();
config.setDefaultLabelProvider(new GerritCompareInputLabelProvider());
return config;
}
@Override
//It supports the following scenarios
//base <-> workspace
//base <-> revision
//revision <-> revision
//workspace <-> base
protected Object prepareInput(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
resetRoot();
nodeToReveal = null;
if (leftSide.equals(rightSide)) {
return root;
}
if ((leftSide.equals(BASE) && rightSide.equals(WORKSPACE))
|| (leftSide.equals(WORKSPACE) && rightSide.equals(BASE))) {
compareWorkspaceWithBase();
} else if (leftSide.equals(BASE) || rightSide.equals(BASE)) {
compareRevisionWithBase();
} else if (leftSide.equals(WORKSPACE) || rightSide.equals(WORKSPACE)) {
compareRevisionWithWorkspace();
} else {
compareRevisions();
}
return root;
}
private void compareWorkspaceWithBase() {
loadRevision(changeInfo.getRevision().getId());
RevisionInfo rightRevision = changeInfo.getRevisions().get(changeInfo.getRevision().getId());
for (FileInfo rightFile : rightRevision.getFiles().values()) {
GerritDiffNode node = createBaseWorkspaceNode(rightFile);
root.add(node);
setElementToReveal(node, rightFile);
}
}
void switchInputs(String left, String right) {
try {
//Short-circuit if nothing has changed
if (left == null && rightSide.equals(right)) {
return;
}
if (right == null && leftSide.equals(left)) {
return;
}
//First, remember the currently opened file.
Object selectedElement = getSelectedEdition();
if (selectedElement != null) {
fileToReveal = ((GerritDiffNode) selectedElement).getFileInfo();
}
//Reset the input
if (left != null) {
this.leftSide = left;
}
if (right != null) {
this.rightSide = right;
}
//Compute the diffs
prepareInput(new NullProgressMonitor());
upperSection.setInput(root);
} catch (InvocationTargetException | InterruptedException e) {
logger.error("Problem while switching input to " + left + " " + right, e); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private void resetRoot() {
IDiffElement[] children = root.getChildren();
for (IDiffElement child : children) {
root.remove(child);
}
}
private void compareRevisions() {
loadRevision(leftSide);
loadRevision(rightSide);
Map<String, FileInfo> files = loadRevisionDiff(rightSide, leftSide);
for (Entry<String, FileInfo> file : files.entrySet()) {
GerritDiffNode node = createRevisionRevisionNode(changeInfo.getRevisions().get(leftSide),
changeInfo.getRevisions().get(rightSide), file.getValue(), file.getKey());
if (node != null) {
root.add(node);
setElementToReveal(node, node.getFileInfo());
}
}
}
private Map<String, FileInfo> loadRevisionDiff(String referenceRevision, String base) {
Map<String, FileInfo> files = null;
try {
files = gerritClient.getFilesModifiedSince(changeInfo.getId(), referenceRevision, base).call();
} catch (EGerritException e) {
logger.debug("An exception occurred while getting the diff between revision " + referenceRevision + " and " //$NON-NLS-1$//$NON-NLS-2$
+ base, e);
files = new HashMap<>();
}
return files;
}
private GerritDiffNode createRevisionRevisionNode(RevisionInfo leftRevision, RevisionInfo rightRevision,
FileInfo fileToShow, String filePathToShow) {
int differenceKind = getDifferenceFlag(fileToShow);
GerritDiffNode node = new GerritDiffNode(differenceKind);
node.setDiffFileInfo(fileToShow);
ITypedElement leftFile = null;
ITypedElement rightFile = null;
switch (differenceKind) {
case GerritDifferences.ADDITION:
node.setFileInfo(leftRevision.getFiles().get(filePathToShow));
FileInfo potentialRight = rightRevision.getFiles().get(filePathToShow);
if (potentialRight == null) {
//Real addition
leftFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(leftRevision.getFiles().get(filePathToShow));
rightFile = new EmptyTypedElement(filePathToShow);
} else {
//Deal with the case where a deletion that happened between revision 1 and 3 is shown as an addition
//because we are comparing revision 3 with 1 (E.g. review 83696).
leftFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(leftRevision.getFiles().get(filePathToShow));
rightFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(rightRevision.getFiles().get(filePathToShow));
}
break;
case GerritDifferences.RENAMED:
case GerritDifferences.COPIED:
if (leftRevision.getFiles().get(filePathToShow) != null) {
leftFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(leftRevision.getFiles().get(filePathToShow));
node.setFileInfo(leftRevision.getFiles().get(filePathToShow));
} else {
OrphanedFileInfo o = new OrphanedFileInfo();
o.setFilePath(filePathToShow);
o.setRevisionInfo(leftRevision);
String baseCommitId = getBaseCommitId(o);
if (baseCommitId != null) {
leftFile = new CompareItemFactory(gerritClient).createCompareItemFromCommit(leftRevision, o,
filePathToShow, leftRevision.get_number());
} else {
leftFile = new EmptyTypedElement(fileToShow.getOld_path());
}
node.setFileInfo(o);
}
if (rightRevision.getFiles().get(fileToShow.getOld_path()) != null) {
rightFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(rightRevision.getFiles().get(fileToShow.getOld_path()));
} else {
OrphanedFileInfo o = new OrphanedFileInfo();
o.setFilePath(fileToShow.getOld_path());
o.setOld_path(fileToShow.getOld_path());
o.setRevisionInfo(rightRevision);
o.setStatus(fileToShow.getStatus());
o.setReviewed(fileToShow.isReviewed());
String baseCommitId = getBaseCommitId(o);
if (baseCommitId != null) {
rightFile = new CompareItemFactory(gerritClient).createCompareItemFromCommit(rightRevision, o,
fileToShow.getOld_path(), rightRevision.get_number());
} else {
rightFile = new EmptyTypedElement(fileToShow.getOld_path());
}
}
break;
case GerritDifferences.CHANGE:
if (leftRevision.getFiles().get(filePathToShow) == null) {
leftFile = new CompareItemFactory(gerritClient).createCompareItemFromCommit(leftRevision,
rightRevision.getFiles().get(filePathToShow), null, leftRevision.get_number());
} else {
leftFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(leftRevision.getFiles().get(filePathToShow));
}
if (rightRevision.getFiles().get(filePathToShow) == null) {
rightFile = new CompareItemFactory(gerritClient).createCompareItemFromCommit(rightRevision,
leftRevision.getFiles().get(filePathToShow), null, rightRevision.get_number());
node.setFileInfo(leftRevision.getFiles().get(filePathToShow));
} else {
rightFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(rightRevision.getFiles().get(filePathToShow));
node.setFileInfo(rightRevision.getFiles().get(filePathToShow));
}
break;
case GerritDifferences.DELETION:
leftFile = new EmptyTypedElement(filePathToShow);
rightFile = new CompareItemFactory(gerritClient)
.createCompareItemFromRevision(rightRevision.getFiles().get(filePathToShow));
node.setFileInfo(rightRevision.getFiles().get(filePathToShow));
break;
default:
return null;
}
node.setLeft(leftFile);
node.setRight(rightFile);
return node;
}
private String getBaseCommitId(FileInfo fileInfo) {
return fileInfo.getRevision().getBaseCommit();
}
private int getDifferenceFlag(FileInfo file) {
switch (file.getStatus()) {
case "A": //$NON-NLS-1$
return GerritDifferences.ADDITION;
case "D": //$NON-NLS-1$
return GerritDifferences.DELETION;
case "C": //$NON-NLS-1$
return GerritDifferences.COPIED;
case "W": //$NON-NLS-1$
return GerritDifferences.REWRITTEN;
case "R": //$NON-NLS-1$
return GerritDifferences.RENAMED;
case "M": //$NON-NLS-1$
default:
return GerritDifferences.CHANGE;
}
}
private void compareRevisionWithWorkspace() {
boolean workspaceOnRight = rightSide.equals(WORKSPACE);
loadRevision(workspaceOnRight ? leftSide : rightSide);
RevisionInfo filesToShow = changeInfo.getRevisions().get(workspaceOnRight ? leftSide : rightSide);
for (FileInfo file : filesToShow.getFiles().values()) {
GerritDiffNode node = createWorkspaceRevisionNode(file);
root.add(node);
setElementToReveal(node, file);
}
}
private void loadRevision(String revision) {
//Load the files synchronously so we can start filling the UI and then load the rest in the background
QueryHelpers.loadFiles(gerritClient, changeInfo.getRevisions().get(revision));
//Load the revision details to be able to completely fill the upper section of the compare editor
CompletableFuture.runAsync(
() -> QueryHelpers.loadRevisionDetails(gerritClient, changeInfo.getRevisions().get(revision)));
}
private GerritDiffNode createWorkspaceRevisionNode(FileInfo file) {
boolean workspaceOnRight = rightSide.equals(WORKSPACE);
String fileName = file.getPath();
GerritDiffNode node = new GerritDiffNode(getDifferenceFlag(file));
node.setFileInfo(file);
PatchSetCompareItem revisionFile = new CompareItemFactory(gerritClient).createCompareItemFromRevision(file);
IFile workspaceFile = new OpenCompareEditor(gerritClient, changeInfo).getCorrespondingWorkspaceFile(file);
if (workspaceFile == null) {
workspaceFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path("missing/" + fileName)); //$NON-NLS-1$
}
ITypedElement workspaceNode = createFileElement(workspaceFile);
if (workspaceOnRight) {
node.setRight(workspaceNode);
node.setLeft(revisionFile);
} else {
node.setRight(revisionFile);
node.setLeft(workspaceNode);
}
return node;
}
private void compareRevisionWithBase() {
String revisionToCompareAgainst = leftSide.equals(BASE) ? rightSide : leftSide;
loadRevision(revisionToCompareAgainst);
RevisionInfo rightRevision = changeInfo.getRevisions().get(revisionToCompareAgainst);
for (FileInfo rightFile : rightRevision.getFiles().values()) {
GerritDiffNode node = createBaseRevisionNode(revisionToCompareAgainst, rightFile);
root.add(node);
setElementToReveal(node, rightFile);
}
}
private GerritDiffNode createBaseWorkspaceNode(FileInfo file) {
String fileName = file.getPath();
GerritDiffNode node = new GerritDiffNode(getDifferenceFlag(file));
node.setFileInfo(file);
//create the node for the workspace file
IFile workspaceFile = new OpenCompareEditor(gerritClient, changeInfo).getCorrespondingWorkspaceFile(file);
if (workspaceFile == null) {
workspaceFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path("missing/" + fileName)); //$NON-NLS-1$
}
ITypedElement workspaceElement = createFileElement(workspaceFile);
//create the node for the base file
String baseCommitId = getBaseCommitId(file);
ITypedElement baseElement = null;
if (baseCommitId != null) {
baseElement = new CompareItemFactory(gerritClient).createCompareItemFromBase(changeInfo.getRevision(), file,
fileName);
} else {
baseElement = new EmptyTypedElement(""); //$NON-NLS-1$
}
if (rightSide.equals(WORKSPACE)) {
node.setRight(workspaceElement);
node.setLeft(baseElement);
} else {
node.setRight(baseElement);
node.setLeft(workspaceElement);
}
return node;
}
private GerritDiffNode createBaseRevisionNode(String revisionToCompareAgainst, FileInfo file) {
String fileName = file.getPath();
GerritDiffNode node = new GerritDiffNode(getDifferenceFlag(file));
node.setFileInfo(file);
PatchSetCompareItem revisionElement = new CompareItemFactory(gerritClient).createCompareItemFromRevision(file);
String baseCommitId = getBaseCommitId(file);
ITypedElement baseElement = null;
if (baseCommitId != null) {
baseElement = new CompareItemFactory(gerritClient)
.createCompareItemFromBase(changeInfo.getRevisions().get(revisionToCompareAgainst), file, fileName);
} else {
baseElement = new EmptyTypedElement(""); //$NON-NLS-1$
}
if (leftSide.equals(BASE)) {
node.setLeft(baseElement);
node.setRight(revisionElement);
} else {
node.setLeft(revisionElement);
node.setRight(baseElement);
}
return node;
}
//Test if the given node representing the given file should be revealed in the UI
private void setElementToReveal(GerritDiffNode node, FileInfo file) {
if (fileToReveal == null) {
return;
}
if (file.getPath().equals(fileToReveal.getPath())) {
nodeToReveal = node;
preloadNode(node);
}
}
private void preloadNode(GerritDiffNode node) {
if (node.getLeft() instanceof IStreamContentAccessor) {
try {
((IStreamContentAccessor) node.getLeft()).getContents();
} catch (CoreException e) {
logger.debug("Problem preloading left", e); //$NON-NLS-1$
}
}
if (node.getRight() instanceof IStreamContentAccessor) {
try {
((IStreamContentAccessor) node.getRight()).getContents();
} catch (CoreException e) {
logger.debug("Problem preloading right", e); //$NON-NLS-1$
}
}
}
@Override
public String getTitle() {
return NLS.bind(Messages.CompareEditorTitle,
new Object[] { changeInfo.get_number(), changeInfo.getSubject(), getComparisonTitle() });
}
private String getComparisonTitle() {
if (UICompareUtils.isMirroredOn(this)) {
return getUserReadableString(rightSide) + " / " + getUserReadableString(leftSide); //$NON-NLS-1$
}
return getUserReadableString(leftSide) + " / " + getUserReadableString(rightSide); //$NON-NLS-1$
}
private String getUserReadableString(String string) {
if (string.equals(BASE) || string.equals(WORKSPACE)) {
return string;
}
return Messages.Patchset + changeInfo.getRevisions().get(string).get_number();
}
private LocalResourceTypedElement cachedElement;
@Override
//We need this so we can hook the mechanism to color the comments
public Viewer findContentViewer(Viewer oldViewer, ICompareInput input, Composite parent) {
Viewer newViewer = super.findContentViewer(oldViewer, input, parent);
if (newViewer != null && newViewer.getClass().getName().contains("PhpMergeViewer")) { //$NON-NLS-1$
ViewerDescriptor[] vds = CompareUIPlugin.getDefault().findContentViewerDescriptor(oldViewer, input, null);
setContentViewerDescriptor(vds[vds.length - 1]);
newViewer = super.findContentViewer(oldViewer, input, parent);
}
purgeCache();
//Force a reset of the documents before they get used. This is necessary if the document has already been opened.
if (input instanceof GerritDiffNode) {
GerritDiffNode node = (GerritDiffNode) input;
if (node.getLeft() instanceof CommentableCompareItem) {
((CommentableCompareItem) node.getLeft()).reset();
}
if (node.getRight() instanceof CommentableCompareItem) {
((CommentableCompareItem) node.getRight()).reset();
}
if (node.getLeft() instanceof LocalResourceTypedElement) {
addToCache((LocalResourceTypedElement) node.getLeft());
}
if (node.getRight() instanceof LocalResourceTypedElement) {
addToCache((LocalResourceTypedElement) node.getRight());
}
}
if (oldViewer == newViewer) {
return newViewer;
}
setupCommentColorer(newViewer, 0);
setupCommentColorer(newViewer, 1);
UICompareUtils.insertAnnotationNavigationCommands(CompareViewerPane.getToolBarManager(parent));
return newViewer;
}
//Load the IFile represented by the typedElement in the FileBufferManager
//This is necessary to avoid the loss of edits (done on workspace files) when swapping sides.
//Because the compare editor only shows one file at a time, we only add one element.
private void addToCache(LocalResourceTypedElement typedElement) {
try {
cachedElement = typedElement;
FileBuffers.getTextFileBufferManager().connect(cachedElement.getResource().getFullPath(),
LocationKind.IFILE, new NullProgressMonitor());
} catch (CoreException e) {
//Ignore
}
}
//Remove a file from the FileBufferManager
private void purgeCache() {
if (cachedElement != null) {
try {
FileBuffers.getTextFileBufferManager().disconnect(cachedElement.getResource().getFullPath(),
LocationKind.IFILE, new NullProgressMonitor());
} catch (CoreException e) {
//Ignore
}
}
}
@Override
public Viewer findStructureViewer(Viewer oldViewer, ICompareInput input, Composite parent) {
if (input instanceof GerritDiffNode) {
nodeToReveal = (GerritDiffNode) input;
}
return super.findStructureViewer(oldViewer, input, parent);
}
private WeakInterningHashSet<SourceViewer> decoratedViewers = new WeakInterningHashSet<>(3);
private CompareUpperSection upperSection;
private void setupCommentColorer(Viewer contentViewer, int side) {
if (!(contentViewer instanceof TextMergeViewer)) {
return;
}
//Navigate from the top level widget of the compare editor down to the right pane of the editor
//to participate in the coloring of the text: see method applyTextPresentation
TextMergeViewer textMergeViewer = (TextMergeViewer) contentViewer;
try {
Class<TextMergeViewer> clazz = TextMergeViewer.class;
Field declaredField = clazz.getDeclaredField(side == 0 ? "fLeft" : "fRight"); //$NON-NLS-1$ //$NON-NLS-2$
declaredField.setAccessible(true);
@SuppressWarnings("restriction")
MergeSourceViewer rightSourceViewer = (MergeSourceViewer) declaredField.get(textMergeViewer);
@SuppressWarnings("restriction")
Field sourceViewerField = MergeSourceViewer.class.getDeclaredField("fSourceViewer"); //$NON-NLS-1$
sourceViewerField.setAccessible(true);
final SourceViewer sourceViewer = (SourceViewer) sourceViewerField.get(rightSourceViewer);
if (decoratedViewers.contains(sourceViewer)) {
return;
}
decoratedViewers.add(sourceViewer);
//This listener is responsible to add / remove the listeners that take care
//of the coloring and prevent the edition. Those various listeners are different
//for each document
sourceViewer.addTextInputListener(sourceTextInputListener(side, textMergeViewer, sourceViewer));
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException t) {
logger.error("Problem while setting up coloration of comments", t); //$NON-NLS-1$
}
}
/**
* @param side
* @param textMergeViewer
* @param sourceViewer
* @return
*/
private ITextInputListener sourceTextInputListener(int side, TextMergeViewer textMergeViewer,
final SourceViewer sourceViewer) {
return new ITextInputListener() {
final SourceViewer sv = sourceViewer;
AnnotationPainter commentPainter;
EditionLimiter editionLimiter;
VerifyListener blockEdition;
@Override
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
sv.setEditable(true);
if (newInput instanceof CommentableCompareItem) {
commentPainter = initializeCommentColoring(sv);
editionLimiter = new EditionLimiter(sv);
sv.addTextPresentationListener(commentPainter);
sv.addPainter(commentPainter);
sv.getTextWidget().addVerifyListener(editionLimiter);
//Swapping causes loss of the dirty state, which means that saving won't work properly.
//Therefore we need to force reset the dirty flag based on the edits made to the documents
setDirty(isDirty((CommentableCompareItem) newInput), side);
} else {
if (newInput == null) {
return;
}
/**
* This is our way to prevent edition when the compare editor shows the detailed structural
* pane. What we refer to "the detailed structural diff" is a pane of the compare editor that
* shows structural differences of the file being compared. For example, when you compare a
* .properties file, this pane will show a list of the properties being added or removed.<br/>
* When the user selects an item in the detailed structural pane, the compare editor framework
* recreates a new document from the document already loaded and redisplays this. This causes
* EGerrit issues because we have our own type of document which includes additional information
* like the position of comments, and losing those means that we display the comments without
* the syntax coloring which is confusing to the user. At this point we assume we can't change
* the compare fwk, and instead we detect the situation, and warn the user this usecase is not
* supported. <br/>
*/
if (side == 0) {
//Left side
if (!UICompareUtils.isMirroredOn(GerritMultipleInput.this) && leftSide.equals(WORKSPACE)) {
return;
}
if (UICompareUtils.isMirroredOn(GerritMultipleInput.this) && rightSide.equals(WORKSPACE)) {
return;
}
} else {
if (!UICompareUtils.isMirroredOn(GerritMultipleInput.this) && rightSide.equals(WORKSPACE)) {
//right side
return;
}
if (UICompareUtils.isMirroredOn(GerritMultipleInput.this) && leftSide.equals(WORKSPACE)) {
return;
}
}
blockEdition = new VerifyListener() {
boolean messageShown = false;
@Override
public void verifyText(VerifyEvent e) {
e.doit = false;
if (!messageShown) {
MessageDialog.openInformation(null, Messages.UnsupportedInput_Title,
Messages.UnsupportedInput_Text);
messageShown = true;
}
}
};
sv.getTextWidget().addVerifyListener(blockEdition);
}
}
@Override
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
if (oldInput instanceof CommentableCompareItem) {
if (commentPainter != null) {
sv.removePainter(commentPainter);
sv.removeTextPresentationListener(commentPainter);
}
if (editionLimiter != null) {
sv.getTextWidget().removeVerifyListener(editionLimiter);
}
} else {
if (blockEdition != null) {
sv.getTextWidget().removeVerifyListener(blockEdition);
}
}
}
private AnnotationPainter initializeCommentColoring(ISourceViewer viewer) {
return new CommentAnnotationPainter(viewer, null);
}
//Determine if a document is dirty by checking the status of the comment annotations
private boolean isDirty(CommentableCompareItem doc) {
Iterator<Annotation> iterator = doc.getEditableComments().getAnnotationIterator();
while (iterator.hasNext()) {
GerritCommentAnnotation annotation = (GerritCommentAnnotation) iterator.next();
if (annotation.getComment() == null) {
return true;
}
}
return false;
}
//Helper method to set the dirty flag on the ContentMergeViewer
private void setDirty(boolean dirty, int side) {
try {
Class<ContentMergeViewer> clazz = ContentMergeViewer.class;
Method declaredMethod;
declaredMethod = clazz.getDeclaredMethod(side == 0 ? "setLeftDirty" : "setRightDirty", //$NON-NLS-1$ //$NON-NLS-2$
boolean.class);
declaredMethod.setAccessible(true);
declaredMethod.invoke(textMergeViewer, dirty);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
//Should not happen
}
}
};
}
@Override
public void saveChanges(IProgressMonitor monitor) throws CoreException {
try {
super.saveChanges(monitor);
forceSaveWorkspaceFile(monitor);
} catch (RuntimeException ex) {
//This works hand in hand with the PatchSetCompareItem#setContent method which raises a very specific RuntimeException
if (CommentableCompareItem.class.getName().equals(ex.getMessage())) {
String leftHash = Integer.toString(((GerritDiffNode) getSelectedEdition()).getLeft().hashCode());
if (ex.getCause().getMessage().equals(leftHash)) {
problemSavingChanges = 0;
setLeftDirty(true);
} else {
problemSavingChanges = 1;
setRightDirty(true);
}
throw new CoreException(new org.eclipse.core.runtime.Status(IStatus.ERROR, EGerritUIPlugin.PLUGIN_ID,
Messages.GerritMultipleInput_11));
} else {
//When it is not our exception we just pass it on.
throw ex;
}
}
if (postSaveListener != null) {
postSaveListener.run();
}
}
private void forceSaveWorkspaceFile(IProgressMonitor monitor) throws CoreException {
//Force persist the files that are in the workspace.
//This is necessary because the Saveable returned by default is not properly set
if (getSelectedEdition() != null) {
ITypedElement forceLeft = ((GerritDiffNode) getSelectedEdition()).getLeft();
if (forceLeft instanceof LocalResourceTypedElement) {
((LocalResourceTypedElement) forceLeft).commit(monitor);
}
ITypedElement forceright = ((GerritDiffNode) getSelectedEdition()).getRight();
if (forceright instanceof LocalResourceTypedElement) {
((LocalResourceTypedElement) forceright).commit(monitor);
}
}
}
@Override
public void setDirty(boolean dirty) {
if (problemSavingChanges == 0) {
super.setDirty(true);
problemSavingChanges = -1;
setLeftDirty(true);
} else if (problemSavingChanges == 1) {
super.setDirty(true);
problemSavingChanges = -1;
setRightDirty(true);
} else {
super.setDirty(dirty);
}
}
@Override
public String getName() {
if (fileToReveal != null && (StringToFileInfoImpl) fileToReveal.eContainer() != null) {
return ((StringToFileInfoImpl) fileToReveal.eContainer()).getKey();
}
return ""; //$NON-NLS-1$
}
@Override
public void fireInputChange() {
//There are 6 cases to deal with. The table below captures all the permutations.
//R stands for revision, W for workspace and B for base
// left right
//1 R R
//2 R W
//3 B R
//4 B W
//5 W W
//6 W R
GerritDiffNode savedElement = (GerritDiffNode) getSelectedEdition();
//Here we just refresh the content of the node that has just been saved.
GerritDiffNode newEntry = null;
//Deals with the cases 1, 3, 6
if ((leftSide.equals(BASE) && rightSide.equals(WORKSPACE))
|| (leftSide.equals(WORKSPACE) && rightSide.equals(BASE))) {
newEntry = createBaseWorkspaceNode(savedElement.getFileInfo());
} else if (leftSide.equals(BASE) || rightSide.equals(BASE)) {
newEntry = createBaseRevisionNode(leftSide.equals(BASE) ? rightSide : leftSide, savedElement.getFileInfo());
} else if (leftSide.equals(WORKSPACE) || rightSide.equals(WORKSPACE)) {
newEntry = createWorkspaceRevisionNode(savedElement.getFileInfo());
} else {
loadRevision(leftSide);
loadRevision(rightSide);
newEntry = createRevisionRevisionNode(changeInfo.getRevisions().get(leftSide),
changeInfo.getRevisions().get(rightSide), savedElement.getDiffFileInfo(),
savedElement.getFileInfo().getPath());
}
if (newEntry != null) {
savedElement.setRight(newEntry.getRight());
savedElement.setLeft(newEntry.getLeft());
}
savedElement.fireChange();
}
@Override
protected ICompareInput prepareCompareInput(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
// ignore
return null;
}
@Override
protected CompareViewerPane createStructureInputPane(final Composite parent) {
upperSection = new CompareUpperSection(parent, SWT.BORDER | SWT.FLAT, true, this);
return upperSection;
}
public ChangeInfo getChangeInfo() {
return changeInfo;
}
public String getLeftSide() {
return leftSide;
}
public String getRightSide() {
return rightSide;
}
public CompareUpperSection getUpperSection() {
return upperSection;
}
@Override
protected void handleDispose() {
purgeCache();
super.handleDispose();
}
@Override
public void registerContextMenu(MenuManager menu, final ISelectionProvider selectionProvider) {
super.registerContextMenu(menu, selectionProvider);
registerOpenWorkspaceVersion(menu, selectionProvider);
}
private void registerOpenWorkspaceVersion(MenuManager menu, final ISelectionProvider selectionProvider) {
menu.addMenuListener(manager -> {
if (!(selectionProvider instanceof SourceViewer) && !(getSelectedEdition() instanceof GerritDiffNode)) {
return;
}
manager.insertAfter("file", //$NON-NLS-1$
new OpenWorkspaceFile((SourceViewer) selectionProvider, (GerritDiffNode) getSelectedEdition(),
gerritClient));
});
}
}