blob: 94c4ca0db4bf9a2cf49047d8e10eeadd6e75070b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2016 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.compare;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.resources.IResource;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.ui.services.IDisposable;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IEditableContentExtension;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.DocumentRangeNode;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.compare.structuremergeviewer.IStructureComparator;
import org.eclipse.compare.structuremergeviewer.StructureCreator;
import org.eclipse.compare.structuremergeviewer.StructureRootNode;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
public class JavaStructureCreator extends StructureCreator {
private Map<String, String> fDefaultCompilerOptions;
/**
* A root node for the structure. It is similar to {@link StructureRootNode} but needed
* to be a subclass of {@link JavaNode} because of the code used to build the structure.
*/
private final class RootJavaNode extends JavaNode implements IDisposable {
private final Object fInput;
private final boolean fEditable;
private final ISharedDocumentAdapter fAdapter;
private RootJavaNode(IDocument document, boolean editable, Object input, ISharedDocumentAdapter adapter) {
super(document);
this.fEditable = editable;
fInput= input;
fAdapter= adapter;
}
@Override
public boolean isEditable() {
return fEditable;
}
@Override
protected void nodeChanged(DocumentRangeNode node) {
save(this, fInput);
}
@Override
public void dispose() {
if (fAdapter != null) {
fAdapter.disconnect(fInput);
}
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter == ISharedDocumentAdapter.class) {
return (T) fAdapter;
}
return super.getAdapter(adapter);
}
@Override
public boolean isReadOnly() {
if (fInput instanceof IEditableContentExtension) {
IEditableContentExtension ext = (IEditableContentExtension) fInput;
return ext.isReadOnly();
}
return super.isReadOnly();
}
@Override
public IStatus validateEdit(Shell shell) {
if (fInput instanceof IEditableContentExtension) {
IEditableContentExtension ext = (IEditableContentExtension) fInput;
return ext.validateEdit(shell);
}
return super.validateEdit(shell);
}
}
/**
* RewriteInfos are used temporarily when rewriting the diff tree
* in order to combine similar diff nodes ("smart folding").
*/
static class RewriteInfo {
boolean fIsOut= false;
JavaNode fAncestor= null;
JavaNode fLeft= null;
JavaNode fRight= null;
ArrayList<IDiffElement> fChildren= new ArrayList<>();
void add(IDiffElement diff) {
fChildren.add(diff);
}
void setDiff(ICompareInput diff) {
if (fIsOut)
return;
fIsOut= true;
JavaNode a= (JavaNode) diff.getAncestor();
JavaNode y= (JavaNode) diff.getLeft();
JavaNode m= (JavaNode) diff.getRight();
if (a != null) {
if (fAncestor != null)
return;
fAncestor= a;
}
if (y != null) {
if (fLeft != null)
return;
fLeft= y;
}
if (m != null) {
if (fRight != null)
return;
fRight= m;
}
fIsOut= false;
}
/**
* @return true if some nodes could be successfully combined into one
*/
boolean matches() {
return !fIsOut && fAncestor != null && fLeft != null && fRight != null;
}
}
public JavaStructureCreator() {
}
void setDefaultCompilerOptions(Map<String, String> compilerSettings) {
fDefaultCompilerOptions= compilerSettings;
}
/**
* @return the name that appears in the enclosing pane title bar
*/
@Override
public String getName() {
return CompareMessages.JavaStructureViewer_title;
}
/**
* @param input implement the IStreamContentAccessor interface
* @return a tree of JavaNodes for the given input.
* In case of error null is returned.
*/
@Override
public IStructureComparator getStructure(final Object input) {
String contents= null;
char[] buffer= null;
IDocument doc= CompareUI.getDocument(input);
if (doc == null) {
if (input instanceof IStreamContentAccessor) {
IStreamContentAccessor sca= (IStreamContentAccessor) input;
try {
contents= JavaCompareUtilities.readString(sca);
} catch (CoreException ex) {
// return null indicates the error.
return null;
}
}
if (contents != null) {
int n= contents.length();
buffer= new char[n];
contents.getChars(0, n, buffer, 0);
doc= new Document(contents);
setupDocument(doc);
}
}
return createStructureComparator(input, buffer, doc, null, null);
}
@Override
protected IStructureComparator createStructureComparator(Object element,
IDocument document, ISharedDocumentAdapter sharedDocumentAdapter,
IProgressMonitor monitor) throws CoreException {
return createStructureComparator(element, null, document, sharedDocumentAdapter, monitor);
}
private IStructureComparator createStructureComparator(final Object input, char[] buffer, IDocument doc, ISharedDocumentAdapter adapter, IProgressMonitor monitor) {
String contents;
Map<String, String> compilerOptions= null;
if (input instanceof IResourceProvider) {
IResource resource= ((IResourceProvider) input).getResource();
if (resource != null) {
IJavaElement element= JavaCore.create(resource);
if (element != null) {
IJavaProject javaProject= element.getJavaProject();
if (javaProject != null)
compilerOptions= javaProject.getOptions(true);
}
}
}
if (compilerOptions == null)
compilerOptions= fDefaultCompilerOptions;
if (doc != null) {
boolean isEditable= false;
if (input instanceof IEditableContent)
isEditable= ((IEditableContent) input).isEditable();
// we hook into the root node to intercept all node changes
JavaNode root= new RootJavaNode(doc, isEditable, input, adapter);
if (buffer == null) {
contents= doc.get();
int n= contents.length();
buffer= new char[n];
contents.getChars(0, n, buffer, 0);
}
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
if (compilerOptions != null)
parser.setCompilerOptions(compilerOptions);
parser.setSource(buffer);
parser.setFocalPosition(0);
CompilationUnit cu= (CompilationUnit) parser.createAST(monitor);
cu.accept(new JavaParseTreeBuilder(root, buffer, true));
return root;
}
return null;
}
/**
* Returns the contents of the given node as a string.
* This string is used to test the content of a Java element
* for equality. Is is never shown in the UI, so any string representing
* the content will do.
* @param node must implement the IStreamContentAccessor interface
* @param ignoreWhiteSpace if <code>true</code> all Java white space (including comments) is removed from the contents.
* @return contents for equality test
*/
@Override
public String getContents(Object node, boolean ignoreWhiteSpace) {
if (! (node instanceof IStreamContentAccessor))
return null;
IStreamContentAccessor sca= (IStreamContentAccessor) node;
String content= null;
try {
content= JavaCompareUtilities.readString(sca);
} catch (CoreException ex) {
JavaPlugin.log(ex);
return null;
}
if (!ignoreWhiteSpace)
return content;
// replace whitespace by a single blank
StringBuffer buf= new StringBuffer();
char[] b= content.toCharArray();
// to avoid the trouble when dealing with Unicode
// we use the Java scanner to extract non-whitespace and comment tokens
IScanner scanner= ToolFactory.createScanner(true, false, false, false); // however we request Whitespace and Comments
scanner.setSource(b);
try {
int token;
while ((token= scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
char[] currentTokenSource= scanner.getCurrentTokenSource();
switch (token) {
case ITerminalSymbols.TokenNameCOMMENT_LINE:
case ITerminalSymbols.TokenNameCOMMENT_BLOCK:
case ITerminalSymbols.TokenNameCOMMENT_JAVADOC:
StringBuffer tokenStr= new StringBuffer(currentTokenSource.length);
tokenStr.append(currentTokenSource);
StringTokenizer tokenizer= new StringTokenizer(tokenStr.toString());
while (tokenizer.hasMoreTokens()) {
buf.append(tokenizer.nextToken());
buf.append(' ');
}
break;
default:
buf.append(currentTokenSource);
buf.append(' ');
break;
}
}
content= buf.toString(); // success!
} catch (InvalidInputException ex) {
// NeedWork
}
return content;
}
/**
* @return true since this IStructureCreator can rewrite the diff tree
* in order to fold certain combinations of additions and deletions.
*/
public boolean canRewriteTree() {
return true;
}
/**
* Tries to detect certain combinations of additions and deletions as renames or signature
* changes and folders them into a single node.
*
* @param differencer the differencer
* @param root the diff container root
*/
public void rewriteTree(Differencer differencer, IDiffContainer root) {
HashMap<String, RewriteInfo> map= new HashMap<>(10);
Object[] children= root.getChildren();
for (int i= 0; i < children.length; i++) {
DiffNode diff= (DiffNode) children[i];
JavaNode jn= (JavaNode) diff.getId();
if (jn == null)
continue;
int type= jn.getTypeCode();
// we can only combine methods or constructors
if (type == JavaNode.METHOD || type == JavaNode.CONSTRUCTOR) {
// find or create a RewriteInfo for all methods with the same name
String name= jn.extractMethodName();
RewriteInfo nameInfo= map.get(name);
if (nameInfo == null) {
nameInfo= new RewriteInfo();
map.put(name, nameInfo);
}
nameInfo.add(diff);
// find or create a RewriteInfo for all methods with the same
// (non-empty) argument list
String argList= jn.extractArgumentList();
RewriteInfo argInfo= null;
if (argList != null && !argList.equals("()")) { //$NON-NLS-1$
argInfo= map.get(argList);
if (argInfo == null) {
argInfo= new RewriteInfo();
map.put(argList, argInfo);
}
argInfo.add(diff);
}
switch (diff.getKind() & Differencer.CHANGE_TYPE_MASK) {
case Differencer.ADDITION:
case Differencer.DELETION:
// we only consider addition and deletions
// since a rename or argument list change looks
// like a pair of addition and deletions
if (type != JavaNode.CONSTRUCTOR)
nameInfo.setDiff(diff);
if (argInfo != null)
argInfo.setDiff(diff);
break;
default:
break;
}
}
// recurse
rewriteTree(differencer, diff);
}
// now we have to rebuild the diff tree according to the combined
// changes
Iterator<String> it= map.keySet().iterator();
while (it.hasNext()) {
String name= it.next();
RewriteInfo i= map.get(name);
if (i.matches()) { // we found a RewriteInfo that could be successfully combined
// we have to find the differences of the newly combined node
// (because in the first pass we only got a deletion and an addition)
DiffNode d= (DiffNode) differencer.findDifferences(true, null, root, i.fAncestor, i.fLeft, i.fRight);
if (d != null) {// there better should be a difference
d.setDontExpand(true);
Iterator<IDiffElement> it2= i.fChildren.iterator();
while (it2.hasNext()) {
IDiffElement rd= it2.next();
root.removeToRoot(rd);
d.add(rd);
}
}
}
}
}
/**
* The JavaHistoryAction uses this function to determine whether
* a selected Java element can be replaced by some piece of
* code from the local history.
* @param je Java element
* @return true if the given IJavaElement maps to a JavaNode
*/
static boolean hasEdition(IJavaElement je) {
return JavaElementHistoryPageSource.hasEdition(je);
}
@Override
protected IDocumentPartitioner getDocumentPartitioner() {
return JavaCompareUtilities.createJavaPartitioner();
}
@Override
protected String getDocumentPartitioning() {
return IJavaPartitions.JAVA_PARTITIONING;
}
@Override
protected String[] getPath(Object element, Object input) {
if (element instanceof IJavaElement) {
IJavaElement je = (IJavaElement) element;
// build a path starting at the given Java element and walk
// up the parent chain until we reach a IWorkingCopy or ICompilationUnit
List<String> args= new ArrayList<>();
while (je != null) {
// each path component has a name that uses the same
// conventions as a JavaNode name
String name= JavaCompareUtilities.getJavaElementID(je);
if (name == null)
return null;
args.add(name);
if (je instanceof ICompilationUnit)
break;
je= je.getParent();
}
// revert the path
int n= args.size();
String[] path= new String[n];
for (int i= 0; i < n; i++)
path[i]= args.get(n-1-i);
return path;
}
return null;
}
}