/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package net.sourceforge.phpdt.internal.ui.text.folding;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import net.sourceforge.phpdt.core.ElementChangedEvent;
import net.sourceforge.phpdt.core.ICompilationUnit;
import net.sourceforge.phpdt.core.IElementChangedListener;
import net.sourceforge.phpdt.core.IJavaElement;
import net.sourceforge.phpdt.core.IJavaElementDelta;
import net.sourceforge.phpdt.core.IMember;
import net.sourceforge.phpdt.core.IParent;
import net.sourceforge.phpdt.core.ISourceRange;
import net.sourceforge.phpdt.core.ISourceReference;
import net.sourceforge.phpdt.core.IType;
import net.sourceforge.phpdt.core.JavaCore;
import net.sourceforge.phpdt.core.JavaModelException;
import net.sourceforge.phpdt.core.ToolFactory;
import net.sourceforge.phpdt.core.compiler.IScanner;
import net.sourceforge.phpdt.core.compiler.ITerminalSymbols;
import net.sourceforge.phpdt.core.compiler.InvalidInputException;
import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
import net.sourceforge.phpdt.internal.ui.text.DocumentCharacterIterator;
import net.sourceforge.phpdt.ui.IWorkingCopyManager;
import net.sourceforge.phpdt.ui.PreferenceConstants;
import net.sourceforge.phpdt.ui.text.folding.IJavaFoldingStructureProvider;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;
import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.projection.IProjectionListener;
import org.eclipse.jface.text.source.projection.IProjectionPosition;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
/**
* Updates the projection model of a class file or compilation unit.
*
* @since 3.0
*/
public class DefaultJavaFoldingStructureProvider implements IProjectionListener, IJavaFoldingStructureProvider {
private static class JavaProjectionAnnotation extends ProjectionAnnotation {
private IJavaElement fJavaElement;
private boolean fIsComment;
public JavaProjectionAnnotation(IJavaElement element, boolean isCollapsed, boolean isComment) {
super(isCollapsed);
fJavaElement = element;
fIsComment = isComment;
}
public IJavaElement getElement() {
return fJavaElement;
}
public void setElement(IJavaElement element) {
fJavaElement = element;
}
public boolean isComment() {
return fIsComment;
}
public void setIsComment(boolean isComment) {
fIsComment = isComment;
}
/*
* @see java.lang.Object#toString()
*/
public String toString() {
return "JavaProjectionAnnotation:\n" + //$NON-NLS-1$
"\telement: \t" + fJavaElement.toString() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
"\tcollapsed: \t" + isCollapsed() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
"\tcomment: \t" + fIsComment + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
}
}
private static final class Tuple {
JavaProjectionAnnotation annotation;
Position position;
Tuple(JavaProjectionAnnotation annotation, Position position) {
this.annotation = annotation;
this.position = position;
}
}
private class ElementChangedListener implements IElementChangedListener {
/*
* @see org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse.jdt.core.ElementChangedEvent)
*/
public void elementChanged(ElementChangedEvent e) {
IJavaElementDelta delta = findElement(fInput, e.getDelta());
if (delta != null)
processDelta(delta);
}
private IJavaElementDelta findElement(IJavaElement target, IJavaElementDelta delta) {
if (delta == null || target == null)
return null;
IJavaElement element = delta.getElement();
if (element.getElementType() > IJavaElement.CLASS_FILE)
return null;
if (target.equals(element))
return delta;
IJavaElementDelta[] children = delta.getAffectedChildren();
for (int i = 0; i < children.length; i++) {
IJavaElementDelta d = findElement(target, children[i]);
if (d != null)
return d;
}
return null;
}
}
/**
* Projection position that will return two foldable regions: one folding away
* the region from after the '/**' to the beginning of the content, the other
* from after the first content line until after the comment.
*
* @since 3.1
*/
private static final class CommentPosition extends Position implements IProjectionPosition {
CommentPosition(int offset, int length) {
super(offset, length);
}
/*
* @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
*/
public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
int prefixEnd = 0;
int contentStart = findFirstContent(sequence, prefixEnd);
int firstLine = document.getLineOfOffset(offset + prefixEnd);
int captionLine = document.getLineOfOffset(offset + contentStart);
int lastLine = document.getLineOfOffset(offset + length);
Assert.isTrue(firstLine <= captionLine, "first folded line is greater than the caption line"); //$NON-NLS-1$
Assert.isTrue(captionLine <= lastLine, "caption line is greater than the last folded line"); //$NON-NLS-1$
IRegion preRegion;
if (firstLine < captionLine) {
// preRegion= new Region(offset + prefixEnd, contentStart - prefixEnd);
int preOffset = document.getLineOffset(firstLine);
IRegion preEndLineInfo = document.getLineInformation(captionLine);
int preEnd = preEndLineInfo.getOffset();
preRegion = new Region(preOffset, preEnd - preOffset);
} else {
preRegion = null;
}
if (captionLine < lastLine) {
int postOffset = document.getLineOffset(captionLine + 1);
IRegion postRegion = new Region(postOffset, offset + length - postOffset);
if (preRegion == null)
return new IRegion[] { postRegion };
return new IRegion[] { preRegion, postRegion };
}
if (preRegion != null)
return new IRegion[] { preRegion };
return null;
}
/**
* Finds the offset of the first identifier part within content
.
* Returns 0 if none is found.
*
* @param content
* the content to search
* @return the first index of a unicode identifier part, or zero if none can
* be found
*/
private int findFirstContent(final CharSequence content, int prefixEnd) {
int lenght = content.length();
for (int i = prefixEnd; i < lenght; i++) {
if (Character.isUnicodeIdentifierPart(content.charAt(i)))
return i;
}
return 0;
}
// /**
// * Finds the offset of the first identifier part within
// content
.
// * Returns 0 if none is found.
// *
// * @param content the content to search
// * @return the first index of a unicode identifier part, or zero if none
// can
// * be found
// */
// private int findPrefixEnd(final CharSequence content) {
// // return the index after the leading '/*' or '/**'
// int len= content.length();
// int i= 0;
// while (i < len && isWhiteSpace(content.charAt(i)))
// i++;
// if (len >= i + 2 && content.charAt(i) == '/' && content.charAt(i + 1) ==
// '*')
// if (len >= i + 3 && content.charAt(i + 2) == '*')
// return i + 3;
// else
// return i + 2;
// else
// return i;
// }
//
// private boolean isWhiteSpace(char c) {
// return c == ' ' || c == '\t';
// }
/*
* @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
*/
public int computeCaptionOffset(IDocument document) {
// return 0;
DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
return findFirstContent(sequence, 0);
}
}
/**
* Projection position that will return two foldable regions: one folding away
* the lines before the one containing the simple name of the java element,
* one folding away any lines after the caption.
*
* @since 3.1
*/
private static final class JavaElementPosition extends Position implements IProjectionPosition {
private IMember fMember;
public JavaElementPosition(int offset, int length, IMember member) {
super(offset, length);
Assert.isNotNull(member);
fMember = member;
}
public void setMember(IMember member) {
Assert.isNotNull(member);
fMember = member;
}
/*
* @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeFoldingRegions(org.eclipse.jface.text.IDocument)
*/
public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
int nameStart = offset;
try {
/*
* The member's name range may not be correct. However, reconciling
* would trigger another element delta which would lead to reentrant
* situations. Therefore, we optimistically assume that the name range
* is correct, but double check the received lines below.
*/
ISourceRange nameRange = fMember.getNameRange();
if (nameRange != null)
nameStart = nameRange.getOffset();
} catch (JavaModelException e) {
// ignore and use default
}
int firstLine = document.getLineOfOffset(offset);
int captionLine = document.getLineOfOffset(nameStart);
int lastLine = document.getLineOfOffset(offset + length);
/*
* see comment above - adjust the caption line to be inside the entire
* folded region, and rely on later element deltas to correct the name
* range.
*/
if (captionLine < firstLine)
captionLine = firstLine;
if (captionLine > lastLine)
captionLine = lastLine;
IRegion preRegion;
if (firstLine < captionLine) {
int preOffset = document.getLineOffset(firstLine);
IRegion preEndLineInfo = document.getLineInformation(captionLine);
int preEnd = preEndLineInfo.getOffset();
preRegion = new Region(preOffset, preEnd - preOffset);
} else {
preRegion = null;
}
if (captionLine < lastLine) {
int postOffset = document.getLineOffset(captionLine + 1);
IRegion postRegion = new Region(postOffset, offset + length - postOffset);
if (preRegion == null)
return new IRegion[] { postRegion };
return new IRegion[] { preRegion, postRegion };
}
if (preRegion != null)
return new IRegion[] { preRegion };
return null;
}
/*
* @see org.eclipse.jface.text.source.projection.IProjectionPosition#computeCaptionOffset(org.eclipse.jface.text.IDocument)
*/
public int computeCaptionOffset(IDocument document) throws BadLocationException {
int nameStart = offset;
try {
// need a reconcile here?
ISourceRange nameRange = fMember.getNameRange();
if (nameRange != null)
nameStart = nameRange.getOffset();
} catch (JavaModelException e) {
// ignore and use default
}
return nameStart - offset;
}
}
private IDocument fCachedDocument;
private ProjectionAnnotationModel fCachedModel;
private ITextEditor fEditor;
private ProjectionViewer fViewer;
private IJavaElement fInput;
private IElementChangedListener fElementListener;
private boolean fAllowCollapsing = false;
private boolean fCollapseJavadoc = false;
// private boolean fCollapseImportContainer = true;
private boolean fCollapseInnerTypes = true;
private boolean fCollapseMethods = false;
private boolean fCollapseHeaderComments = true;
/* caches for header comment extraction. */
private IType fFirstType;
private boolean fHasHeaderComment;
public DefaultJavaFoldingStructureProvider() {
}
public void install(ITextEditor editor, ProjectionViewer viewer) {
if (editor instanceof PHPEditor) {
fEditor = editor;
fViewer = viewer;
fViewer.addProjectionListener(this);
}
}
public void uninstall() {
if (isInstalled()) {
projectionDisabled();
fViewer.removeProjectionListener(this);
fViewer = null;
fEditor = null;
}
}
protected boolean isInstalled() {
return fEditor != null;
}
/*
* @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionEnabled()
*/
public void projectionEnabled() {
// http://home.ott.oti.com/teams/wswb/anon/out/vms/index.html
// projectionEnabled messages are not always paired with projectionDisabled
// i.e. multiple enabled messages may be sent out.
// we have to make sure that we disable first when getting an enable
// message.
projectionDisabled();
if (fEditor instanceof PHPEditor) {
initialize();
fElementListener = new ElementChangedListener();
JavaCore.addElementChangedListener(fElementListener);
}
}
/*
* @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionDisabled()
*/
public void projectionDisabled() {
fCachedDocument = null;
if (fElementListener != null) {
JavaCore.removeElementChangedListener(fElementListener);
fElementListener = null;
}
}
public void initialize() {
if (!isInstalled())
return;
initializePreferences();
try {
IDocumentProvider provider = fEditor.getDocumentProvider();
fCachedDocument = provider.getDocument(fEditor.getEditorInput());
fAllowCollapsing = true;
fFirstType = null;
fHasHeaderComment = false;
if (fEditor instanceof PHPUnitEditor) {
IWorkingCopyManager manager = PHPeclipsePlugin.getDefault().getWorkingCopyManager();
fInput = manager.getWorkingCopy(fEditor.getEditorInput());
}
// else if (fEditor instanceof ClassFileEditor) {
// IClassFileEditorInput editorInput= (IClassFileEditorInput)
// fEditor.getEditorInput();
// fInput= editorInput.getClassFile();
// }
if (fInput != null) {
ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
if (model != null) {
fCachedModel = model;
if (fInput instanceof ICompilationUnit) {
ICompilationUnit unit = (ICompilationUnit) fInput;
synchronized (unit) {
try {
// unit.reconcile(ICompilationUnit.NO_AST, false, null, null);
unit.reconcile();
} catch (JavaModelException x) {
}
}
}
Map additions = computeAdditions((IParent) fInput);
/*
* Minimize the events being sent out - as this happens in the UI
* thread merge everything into one call.
*/
List removals = new LinkedList();
Iterator existing = model.getAnnotationIterator();
while (existing.hasNext())
removals.add(existing.next());
model.replaceAnnotations((Annotation[]) removals.toArray(new Annotation[removals.size()]), additions);
}
}
} finally {
fCachedDocument = null;
fCachedModel = null;
fAllowCollapsing = false;
fFirstType = null;
fHasHeaderComment = false;
}
}
private void initializePreferences() {
IPreferenceStore store = PHPeclipsePlugin.getDefault().getPreferenceStore();
fCollapseInnerTypes = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INNERTYPES);
// fCollapseImportContainer =
// store.getBoolean(PreferenceConstants.EDITOR_FOLDING_IMPORTS);
fCollapseJavadoc = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_JAVADOC);
fCollapseMethods = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_METHODS);
fCollapseHeaderComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADERS);
}
private Map computeAdditions(IParent parent) {
Map map = new LinkedHashMap(); // use a linked map to maintain ordering of
// comments
try {
computeAdditions(parent.getChildren(), map);
} catch (JavaModelException x) {
}
return map;
}
private void computeAdditions(IJavaElement[] elements, Map map) throws JavaModelException {
for (int i = 0; i < elements.length; i++) {
IJavaElement element = elements[i];
computeAdditions(element, map);
if (element instanceof IParent) {
IParent parent = (IParent) element;
computeAdditions(parent.getChildren(), map);
}
}
}
private void computeAdditions(IJavaElement element, Map map) {
boolean createProjection = false;
boolean collapse = false;
switch (element.getElementType()) {
// case IJavaElement.IMPORT_CONTAINER:
// collapse = fAllowCollapsing && fCollapseImportContainer;
// createProjection = true;
// break;
case IJavaElement.TYPE:
collapse = fAllowCollapsing;
if (isInnerType((IType) element)) {
collapse = collapse && fCollapseInnerTypes;
}
else {
collapse = false; //don't allow the most outer type to be folded, may be changed in future versions
}
createProjection = true;
break;
case IJavaElement.METHOD:
collapse = fAllowCollapsing && fCollapseMethods;
createProjection = true;
break;
}
if (createProjection) {
IRegion[] regions = computeProjectionRanges(element);
if (regions != null) {
// comments
for (int i = 0; i < regions.length - 1; i++) {
Position position = createProjectionPosition(regions[i], null);
boolean commentCollapse;
if (position != null) {
if (i == 0 && (regions.length > 2 || fHasHeaderComment) && element == fFirstType) {
commentCollapse = fAllowCollapsing && fCollapseHeaderComments;
} else {
commentCollapse = fAllowCollapsing && fCollapseJavadoc;
}
map.put(new JavaProjectionAnnotation(element, commentCollapse, true), position);
}
}
// code
Position position = createProjectionPosition(regions[regions.length - 1], element);
if (position != null)
map.put(new JavaProjectionAnnotation(element, collapse, false), position);
}
}
}
private boolean isInnerType(IType type) {
try {
return type.isMember();
} catch (JavaModelException x) {
IJavaElement parent = type.getParent();
if (parent != null) {
int parentType = parent.getElementType();
return (parentType != IJavaElement.COMPILATION_UNIT && parentType != IJavaElement.CLASS_FILE);
}
}
return false;
}
/**
* Computes the projection ranges for a given IJavaElement
.
* More than one range may be returned if the element has a leading comment
* which gets folded separately. If there are no foldable regions,
* null
is returned.
*
* @param element
* the java element that can be folded
* @return the regions to be folded, or null
if there are none
*/
private IRegion[] computeProjectionRanges(IJavaElement element) {
try {
if (element instanceof ISourceReference) {
ISourceReference reference = (ISourceReference) element;
ISourceRange range = reference.getSourceRange();
String contents = reference.getSource();
if (contents == null)
return null;
List regions = new ArrayList();
// now add all comments first to the regions list
if (fFirstType == null && element instanceof IType) {
fFirstType = (IType) element;
IRegion headerComment = computeHeaderComment(fFirstType);
if (headerComment != null) {
regions.add(headerComment);
fHasHeaderComment = true;
}
}
final int shift = range.getOffset();
int start = shift;
if (element instanceof IType) {
Scanner scanner = ToolFactory.createScanner(true, false, false, false);
scanner.setSource(contents.toCharArray());
scanner.setPHPMode(true);
int token = scanner.getNextToken();
while (token != ITerminalSymbols.TokenNameEOF) {
token = scanner.getNextToken();
start = shift + scanner.getCurrentTokenStartPosition();
switch (token) {
case ITerminalSymbols.TokenNameCOMMENT_PHPDOC:
case ITerminalSymbols.TokenNameCOMMENT_BLOCK: {
int end = shift + scanner.getCurrentTokenEndPosition() + 1;
regions.add(new Region(start, end - start));
}
case ITerminalSymbols.TokenNameCOMMENT_LINE:
continue;
}
}
}
// at the end add the element region
regions.add(new Region(range.getOffset(), range.getLength()));
if (regions.size() > 0) {
IRegion[] result = new IRegion[regions.size()];
regions.toArray(result);
return result;
}
}
} catch (JavaModelException e) {
} catch (InvalidInputException e) {
}
return null;
}
private IRegion computeHeaderComment(IType type) throws JavaModelException {
if (fCachedDocument == null)
return null;
// search at most up to the first type
ISourceRange range = type.getSourceRange();
if (range == null)
return null;
int start = 0;
int end = range.getOffset();
if (fInput instanceof ISourceReference) {
String content;
try {
content = fCachedDocument.get(start, end - start);
} catch (BadLocationException e) {
return null; // ignore header comment in that case
}
/*
* code adapted from CommentFormattingStrategy: scan the header content up
* to the first type. Once a comment is found, accumulate any additional
* comments up to the stop condition. The stop condition is reaching a
* package declaration, import container, or the end of the input.
*/
IScanner scanner = ToolFactory.createScanner(true, false, false, false);
scanner.setSource(content.toCharArray());
int headerStart = -1;
int headerEnd = -1;
try {
boolean foundComment = false;
int terminal = scanner.getNextToken();
while (terminal != ITerminalSymbols.TokenNameEOF
&& !(terminal == ITerminalSymbols.TokenNameclass || terminal == ITerminalSymbols.TokenNameinterface || foundComment)) {
if (terminal == ITerminalSymbols.TokenNameCOMMENT_PHPDOC || terminal == ITerminalSymbols.TokenNameCOMMENT_BLOCK
|| terminal == ITerminalSymbols.TokenNameCOMMENT_LINE) {
if (!foundComment)
headerStart = scanner.getCurrentTokenStartPosition();
headerEnd = scanner.getCurrentTokenEndPosition();
foundComment = true;
}
terminal = scanner.getNextToken();
}
} catch (InvalidInputException ex) {
return null;
}
if (headerEnd != -1) {
return new Region(headerStart, headerEnd - headerStart);
}
}
return null;
}
private Position createProjectionPosition(IRegion region, IJavaElement element) {
if (fCachedDocument == null)
return null;
try {
int start = fCachedDocument.getLineOfOffset(region.getOffset());
int end = fCachedDocument.getLineOfOffset(region.getOffset() + region.getLength());
if (start != end) {
int offset = fCachedDocument.getLineOffset(start);
int endOffset;
if (fCachedDocument.getNumberOfLines() > end + 1)
endOffset = fCachedDocument.getLineOffset(end + 1);
else if (end > start)
endOffset = fCachedDocument.getLineOffset(end) + fCachedDocument.getLineLength(end);
else
return null;
if (element instanceof IMember)
return new JavaElementPosition(offset, endOffset - offset, (IMember) element);
else
return new CommentPosition(offset, endOffset - offset);
}
} catch (BadLocationException x) {
}
return null;
}
protected void processDelta(IJavaElementDelta delta) {
if (!isInstalled())
return;
if ((delta.getFlags() & (IJavaElementDelta.F_CONTENT | IJavaElementDelta.F_CHILDREN)) == 0)
return;
ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
if (model == null)
return;
try {
IDocumentProvider provider = fEditor.getDocumentProvider();
fCachedDocument = provider.getDocument(fEditor.getEditorInput());
fCachedModel = model;
fAllowCollapsing = false;
fFirstType = null;
fHasHeaderComment = false;
Map additions = new HashMap();
List deletions = new ArrayList();
List updates = new ArrayList();
Map updated = computeAdditions((IParent) fInput);
Map previous = createAnnotationMap(model);
Iterator e = updated.keySet().iterator();
while (e.hasNext()) {
JavaProjectionAnnotation newAnnotation = (JavaProjectionAnnotation) e.next();
IJavaElement element = newAnnotation.getElement();
Position newPosition = (Position) updated.get(newAnnotation);
List annotations = (List) previous.get(element);
if (annotations == null) {
additions.put(newAnnotation, newPosition);
} else {
Iterator x = annotations.iterator();
boolean matched = false;
while (x.hasNext()) {
Tuple tuple = (Tuple) x.next();
JavaProjectionAnnotation existingAnnotation = tuple.annotation;
Position existingPosition = tuple.position;
if (newAnnotation.isComment() == existingAnnotation.isComment()) {
if (existingPosition != null && (!newPosition.equals(existingPosition))) {
existingPosition.setOffset(newPosition.getOffset());
existingPosition.setLength(newPosition.getLength());
updates.add(existingAnnotation);
}
matched = true;
x.remove();
break;
}
}
if (!matched)
additions.put(newAnnotation, newPosition);
if (annotations.isEmpty())
previous.remove(element);
}
}
e = previous.values().iterator();
while (e.hasNext()) {
List list = (List) e.next();
int size = list.size();
for (int i = 0; i < size; i++)
deletions.add(((Tuple) list.get(i)).annotation);
}
match(deletions, additions, updates);
Annotation[] removals = new Annotation[deletions.size()];
deletions.toArray(removals);
Annotation[] changes = new Annotation[updates.size()];
updates.toArray(changes);
model.modifyAnnotations(removals, additions, changes);
} finally {
fCachedDocument = null;
fAllowCollapsing = true;
fCachedModel = null;
fFirstType = null;
fHasHeaderComment = false;
}
}
/**
* Matches deleted annotations to changed or added ones. A deleted
* annotation/position tuple that has a matching addition / change is updated
* and marked as changed. The matching tuple is not added (for additions) or
* marked as deletion instead (for changes). The result is that more
* annotations are changed and fewer get deleted/re-added.
*/
private void match(List deletions, Map additions, List changes) {
if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty()))
return;
List newDeletions = new ArrayList();
List newChanges = new ArrayList();
Iterator deletionIterator = deletions.iterator();
while (deletionIterator.hasNext()) {
JavaProjectionAnnotation deleted = (JavaProjectionAnnotation) deletionIterator.next();
Position deletedPosition = fCachedModel.getPosition(deleted);
if (deletedPosition == null)
continue;
Tuple deletedTuple = new Tuple(deleted, deletedPosition);
Tuple match = findMatch(deletedTuple, changes, null);
boolean addToDeletions = true;
if (match == null) {
match = findMatch(deletedTuple, additions.keySet(), additions);
addToDeletions = false;
}
if (match != null) {
IJavaElement element = match.annotation.getElement();
deleted.setElement(element);
deletedPosition.setLength(match.position.getLength());
if (deletedPosition instanceof JavaElementPosition && element instanceof IMember) {
JavaElementPosition jep = (JavaElementPosition) deletedPosition;
jep.setMember((IMember) element);
}
deletionIterator.remove();
newChanges.add(deleted);
if (addToDeletions)
newDeletions.add(match.annotation);
}
}
deletions.addAll(newDeletions);
changes.addAll(newChanges);
}
/**
* Finds a match for tuple
in a collection of annotations. The
* positions for the JavaProjectionAnnotation
instances in
* annotations
can be found in the passed
* positionMap
or fCachedModel
if
* positionMap
is null
.
*
* A tuple is said to match another if their annotations have the same comment * flag and their position offsets are equal. *
*
* If a match is found, the annotation gets removed from
* annotations
.
*
JavaProjectionAnnotation
* @param positionMap
* a Map<Annotation, Position>
or
* null
* @return a matching tuple or null
for no match
*/
private Tuple findMatch(Tuple tuple, Collection annotations, Map positionMap) {
Iterator it = annotations.iterator();
while (it.hasNext()) {
JavaProjectionAnnotation annotation = (JavaProjectionAnnotation) it.next();
if (tuple.annotation.isComment() == annotation.isComment()) {
Position position = positionMap == null ? fCachedModel.getPosition(annotation) : (Position) positionMap.get(annotation);
if (position == null)
continue;
if (tuple.position.getOffset() == position.getOffset()) {
it.remove();
return new Tuple(annotation, position);
}
}
}
return null;
}
private Map createAnnotationMap(IAnnotationModel model) {
Map map = new HashMap();
Iterator e = model.getAnnotationIterator();
while (e.hasNext()) {
Object annotation = e.next();
if (annotation instanceof JavaProjectionAnnotation) {
JavaProjectionAnnotation java = (JavaProjectionAnnotation) annotation;
Position position = model.getPosition(java);
Assert.isNotNull(position);
List list = (List) map.get(java.getElement());
if (list == null) {
list = new ArrayList(2);
map.put(java.getElement(), list);
}
list.add(new Tuple(java, position));
}
}
Comparator comparator = new Comparator() {
public int compare(Object o1, Object o2) {
return ((Tuple) o1).position.getOffset() - ((Tuple) o2).position.getOffset();
}
};
for (Iterator it = map.values().iterator(); it.hasNext();) {
List list = (List) it.next();
Collections.sort(list, comparator);
}
return map;
}
}