package org.erlide.ui.internal.folding;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.preference.IPreferenceStore;
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.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.erlide.engine.ErlangEngine;
import org.erlide.engine.model.ErlElementKind;
import org.erlide.engine.model.ErlModelException;
import org.erlide.engine.model.IErlElement;
import org.erlide.engine.model.IParent;
import org.erlide.engine.model.erlang.IErlComment;
import org.erlide.engine.model.erlang.IErlMember;
import org.erlide.engine.model.erlang.ISourceRange;
import org.erlide.engine.model.erlang.ISourceReference;
import org.erlide.engine.model.root.IElementChangedListener;
import org.erlide.engine.model.root.IErlElementDelta;
import org.erlide.engine.model.root.IErlModel;
import org.erlide.engine.model.root.IErlModule;
import org.erlide.ui.editors.erl.ErlangEditor;
import org.erlide.ui.editors.erl.folding.IErlangFoldingStructureProvider;
import org.erlide.ui.editors.erl.folding.IErlangFoldingStructureProviderExtension;
import org.erlide.ui.internal.DocumentCharacterIterator;
import org.erlide.ui.internal.ErlideUIPlugin;
import org.erlide.ui.prefs.PreferenceConstants;
import org.erlide.ui.util.ErlModelUtils;
import org.erlide.ui.util.PerformanceTuning;
import org.erlide.util.ErlLogger;
import org.erlide.util.SystemConfiguration;
public class DefaultErlangFoldingStructureProvider implements IProjectionListener,
IErlangFoldingStructureProvider, IErlangFoldingStructureProviderExtension {
private static final class ErlangProjectionAnnotation extends ProjectionAnnotation {
private IErlElement fErlElement;
private final boolean fIsComment;
public ErlangProjectionAnnotation(final IErlElement element,
final boolean isCollapsed, final boolean isComment) {
super(isCollapsed);
fErlElement = element;
fIsComment = isComment;
}
public IErlElement getElement() {
return fErlElement;
}
public void setElement(final IErlElement element) {
fErlElement = element;
}
public boolean isComment() {
return fIsComment;
}
/*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ErlangProjectionAnnotation:\n" + //$NON-NLS-1$
"\telement: \t'"
+ fErlElement.toString()
+ "' " + fErlElement.getKind() + "/" + fErlElement.getParent().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 {
ErlangProjectionAnnotation annotation;
Position position;
Tuple(final ErlangProjectionAnnotation ann, final Position pos) {
annotation = ann;
position = pos;
}
}
/**
* Filter for annotations.
*
* @since 3.2
*/
private static interface Filter {
boolean match(ErlangProjectionAnnotation annotation);
}
private static abstract class MatchCollapsedFilter implements Filter {
private final boolean fMatchCollapsed;
public MatchCollapsedFilter(final boolean matchCollapsed) {
fMatchCollapsed = matchCollapsed;
}
public boolean stateMatch(final ErlangProjectionAnnotation annotation) {
return fMatchCollapsed == annotation.isCollapsed();
}
}
private static final class ErlangElementSetFilter extends MatchCollapsedFilter {
private final Set<IErlElement> fSet;
ErlangElementSetFilter(final Set<IErlElement> set, final boolean matchCollapsed) {
super(matchCollapsed);
fSet = set;
}
@Override
public boolean match(final ErlangProjectionAnnotation annotation) {
if (stateMatch(annotation) && !annotation.isComment()
&& !annotation.isMarkedDeleted()) {
final IErlElement element = annotation.getElement();
if (fSet.contains(element)) {
return true;
}
}
return false;
}
}
/**
* 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(final int off, final int len) {
super(off, len);
}
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionPosition#
* computeFoldingRegions(org.eclipse.jface.text.IDocument)
*/
@Override
public IRegion[] computeProjectionRegions(final IDocument document)
throws BadLocationException {
final DocumentCharacterIterator sequence = new DocumentCharacterIterator(
document, offset, offset + length);
final int prefixEnd = 0;
final int contentStart = findFirstContent(sequence, prefixEnd);
final int firstLine = document.getLineOfOffset(offset + prefixEnd);
final int captionLine = document.getLineOfOffset(offset + contentStart);
final 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);
final int preOffset = document.getLineOffset(firstLine);
final IRegion preEndLineInfo = document.getLineInformation(captionLine);
final int preEnd = preEndLineInfo.getOffset();
preRegion = new Region(preOffset, preEnd - preOffset);
} else {
preRegion = null;
}
if (captionLine < lastLine) {
final int postOffset = document.getLineOffset(captionLine + 1);
final 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
* <code>content</code>. 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, final int prefixEnd) {
final 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
// * <code>content</code>. 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 '/**'
// final 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(final char c) {
// return c == ' ' || c == '\t';
// }
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionPosition#
* computeCaptionOffset(org.eclipse.jface.text.IDocument)
*/
@Override
public int computeCaptionOffset(final IDocument document) {
// return 0;
final 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 erlang
* element, one folding away any lines after the caption.
*
* @since 3.1
*/
private static final class ErlangElementPosition extends Position implements
IProjectionPosition {
private IErlMember fMember;
public ErlangElementPosition(final int off, final int len, final IErlMember member) {
super(off, len);
fMember = member;
}
public void setMember(final IErlMember member) {
fMember = member;
}
@Override
public String toString() {
return "ErlangElementPosition " + offset + " " + length + " "
+ fMember.getName();
}
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionPosition#
* computeFoldingRegions(org.eclipse.jface.text.IDocument)
*/
@Override
public IRegion[] computeProjectionRegions(final IDocument document)
throws BadLocationException {
int nameStart = offset;
/*
* 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.
*/
final ISourceRange nameRange = fMember.getNameRange();
if (nameRange != null) {
nameStart = nameRange.getOffset();
}
final int firstLine = document.getLineOfOffset(offset);
int captionLine = document.getLineOfOffset(nameStart);
final 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) {
final int preOffset = document.getLineOffset(firstLine);
final IRegion preEndLineInfo = document.getLineInformation(captionLine);
final int preEnd = preEndLineInfo.getOffset();
preRegion = new Region(preOffset, preEnd - preOffset);
} else {
preRegion = null;
}
if (captionLine < lastLine) {
final int postOffset = document.getLineOffset(captionLine + 1);
final 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;
}
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionPosition#
* computeCaptionOffset(org.eclipse.jface.text.IDocument)
*/
@Override
public int computeCaptionOffset(final IDocument document)
throws BadLocationException {
int nameStart = offset;
final ISourceRange nameRange = fMember.getNameRange();
if (nameRange != null) {
nameStart = nameRange.getOffset();
}
return nameStart - offset;
}
}
private IDocument fCachedDocument;
private ProjectionAnnotationModel fCachedModel;
private ITextEditor fEditor;
private ProjectionViewer fViewer;
IErlModule fModule;
private IElementChangedListener fElementListener;
private boolean fAllowCollapsing = false;
private boolean fFirstTimeInitialCollapse = true;
private boolean fCollapseHeaderComments = true;
private boolean fCollapseComments = false;
private boolean fCollapseClauses = false;
private boolean fCollapseTypespecs = false;
private static class FunctionsFilter extends MatchCollapsedFilter {
public FunctionsFilter(final boolean matchCollapsed) {
super(matchCollapsed);
}
@Override
public boolean match(final ErlangProjectionAnnotation annotation) {
if (stateMatch(annotation) && !annotation.isComment()
&& !annotation.isMarkedDeleted()) {
final IErlElement element = annotation.getElement();
final ErlElementKind kind = element.getKind();
return kind == ErlElementKind.FUNCTION || kind == ErlElementKind.CLAUSE;
}
return false;
}
}
private final class CommentsFilter extends MatchCollapsedFilter {
public CommentsFilter(final boolean matchCollapsed) {
super(matchCollapsed);
}
@Override
public boolean match(final ErlangProjectionAnnotation annotation) {
if (stateMatch(annotation) && annotation.isComment()
&& !annotation.isMarkedDeleted()) {
return true;
}
return false;
}
}
/* filters */
/**
* Member filter, matches nested members (but not top-level types).
*
* @since 3.2
*/
private final Filter fCollapseFunctionsFilter = new FunctionsFilter(false);
/**
* Comment filter, matches comments.
*
* @since 3.2
*/
private final Filter fCollapseCommentsFilter = new CommentsFilter(false);
private final Filter fExpandAllFilter = new Filter() {
@Override
public boolean match(final ErlangProjectionAnnotation annotation) {
return annotation.isCollapsed();
}
};
public DefaultErlangFoldingStructureProvider() {
}
@Override
public void install(final ITextEditor editor, final ProjectionViewer viewer) {
if (editor instanceof ErlangEditor) {
fFirstTimeInitialCollapse = true;
fEditor = editor;
fViewer = viewer;
fViewer.addProjectionListener(this);
final IErlModel mdl = ErlangEngine.getInstance().getModel();
mdl.addModelChangeListener(this);
}
}
@Override
public void uninstall() {
if (isInstalled()) {
projectionDisabled();
fViewer.removeProjectionListener(this);
fViewer = null;
fEditor = null;
ErlangEngine.getInstance().getModel().removeModelChangeListener(this);
}
}
protected boolean isInstalled() {
return fEditor != null;
}
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionListener#
* projectionEnabled()
*/
@Override
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();
initialize();
if (fEditor instanceof ErlangEditor && fModule != null) {
boolean structureKnown = false;
try {
structureKnown = fModule.isStructureKnown();
} catch (final ErlModelException e1) {
}
if (structureKnown) {
final IErlElementDelta d = ErlangEngine
.getInstance()
.getModel()
.createElementDelta(IErlElementDelta.CHANGED,
IErlElementDelta.F_CONTENT, fModule);
processDelta(d);
} else {
try {
fModule.open(null);
} catch (final ErlModelException e) {
ErlLogger.error(e);
}
}
}
}
/*
* @seeorg.eclipse.jface.text.source.projection.IProjectionListener#
* projectionDisabled()
*/
@Override
public void projectionDisabled() {
fCachedDocument = null;
if (fElementListener != null) {
ErlangEngine.getInstance().getModel()
.removeElementChangedListener(fElementListener);
fElementListener = null;
}
}
@Override
public void initialize() {
if (!isInstalled()) {
return;
}
initializePreferences();
try {
fModule = ErlModelUtils.getModule(fEditor.getEditorInput());
} catch (final CoreException e) {
ErlLogger.error(e);
}
}
private void initializePreferences() {
final IPreferenceStore store = ErlideUIPlugin.getDefault().getPreferenceStore();
fAllowCollapsing = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_ENABLED);
fCollapseClauses = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_CLAUSES);
fCollapseHeaderComments = store
.getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADER_COMMENTS);
fCollapseComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_COMMENTS);
fCollapseTypespecs = store
.getBoolean(PreferenceConstants.EDITOR_FOLDING_TYPESPECS);
}
private void computeAdditions(final IErlModule erlModule,
final Map<ErlangProjectionAnnotation, Position> map) {
if (erlModule == null) {
return;
}
try {
computeAdditions((IParent) erlModule, map);
computeAdditions(erlModule.getComments(), map);
} catch (final ErlModelException x) {
ErlLogger.warn(x);
}
}
private void computeAdditions(final IParent parent,
final Map<ErlangProjectionAnnotation, Position> map) {
if (parent == null) {
return;
}
try {
computeAdditions(parent.getChildren(), map);
} catch (final ErlModelException x) {
ErlLogger.warn(x);
}
}
private void computeAdditions(final Collection<? extends IErlElement> elements,
final Map<ErlangProjectionAnnotation, Position> map) throws ErlModelException {
if (elements == null) {
return;
}
for (final IErlElement element : elements) {
computeAdditions(element, map);
if (element instanceof IParent) {
final IParent parent = (IParent) element;
computeAdditions(parent.getChildren(), map);
}
}
}
private void computeAdditions(final IErlElement element,
final Map<ErlangProjectionAnnotation, Position> map) {
boolean createProjection = false;
boolean collapse = false;
if (element.getKind() == ErlElementKind.CLAUSE
|| element.getKind() == ErlElementKind.FUNCTION) {
collapse = fAllowCollapsing && fCollapseClauses;
createProjection = true;
} else if (element.getKind() == ErlElementKind.COMMENT) {
final IErlComment c = (IErlComment) element;
if (c.isHeader()) {
collapse = fAllowCollapsing && fCollapseHeaderComments;
} else {
collapse = fAllowCollapsing && fCollapseComments;
}
createProjection = true;
} else if (element.getKind() == ErlElementKind.ATTRIBUTE) {
createProjection = true;
} else if (element.getKind() == ErlElementKind.EXPORT) {
createProjection = true;
} else if (element.getKind() == ErlElementKind.RECORD_DEF) {
createProjection = true;
} else if (element.getKind() == ErlElementKind.MACRO_DEF) {
createProjection = true;
} else if (element.getKind() == ErlElementKind.TYPESPEC) {
collapse = fAllowCollapsing && fCollapseTypespecs;
createProjection = true;
}
if (createProjection) {
final IRegion region = computeProjectionRanges(element);
if (region != null) {
final Position position = createProjectionPosition(region, element);
if (position != null) {
map.put(new ErlangProjectionAnnotation(element, collapse
&& fFirstTimeInitialCollapse, element instanceof IErlComment),
position);
}
}
}
}
// private boolean isInnerType(IType type) {
// return type.getDeclaringType() != null;
// }
/**
* Computes the projection ranges for a given <code>IErlElement</code>. More
* than one range may be returned if the element has a leading comment which
* gets folded separately. If there are no foldable regions,
* <code>null</code> is returned.
*
* @param element
* the erlang element that can be folded
* @return the regions to be folded, or <code>null</code> if there are none
*/
private IRegion computeProjectionRanges(final IErlElement element) {
if (element instanceof ISourceReference) {
final ISourceReference reference = (ISourceReference) element;
final ISourceRange range = reference.getSourceRange();
return new Region(range.getOffset(), range.getLength());
}
return null;
}
private Position createProjectionPosition(final IRegion region,
final IErlElement element) {
if (fCachedDocument == null) {
return null;
}
try {
final int roffset = region.getOffset();
int rlength = region.getLength();
if (element instanceof IErlComment
&& SystemConfiguration.getInstance().isOnWindows()) {
// Erlang has CR included in comment
rlength -= 1;
}
final int startLine = fCachedDocument.getLineOfOffset(roffset);
final int endLine = fCachedDocument
.getLineOfOffset(roffset + rlength);
if (startLine != endLine) {
final int offset = fCachedDocument.getLineOffset(startLine);
int endOffset;
if (fCachedDocument.getNumberOfLines() > endLine + 1) {
endOffset = fCachedDocument.getLineOffset(endLine + 1);
} else if (endLine > startLine) {
endOffset = fCachedDocument.getLineOffset(endLine)
+ fCachedDocument.getLineLength(endLine);
} else {
return null;
}
final int length = endOffset - offset;
if (element instanceof IErlComment) {
return new CommentPosition(offset, length);
}
if (element instanceof IErlMember) {
return new ErlangElementPosition(offset, length,
(IErlMember) element);
}
}
} catch (final BadLocationException x) {
}
return null;
}
protected void processDelta(final IErlElementDelta delta) {
if (!isInstalled()) {
return;
}
if ((delta.getFlags() & (IErlElementDelta.F_CONTENT | IErlElementDelta.F_CHILDREN)) == 0) {
return;
}
final IErlElement de = delta.getElement();
if (de instanceof IErlModule && de != fModule) {
return;
}
final ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor
.getAdapter(ProjectionAnnotationModel.class);
if (model == null) {
return;
}
final IDocumentProvider provider = fEditor.getDocumentProvider();
try {
fCachedModel = model;
fCachedDocument = provider.getDocument(fEditor.getEditorInput());
if (fCachedDocument.getNumberOfLines() > PerformanceTuning.get()
.getFoldingLimit()) {
// disable folding for files larger than this
model.removeAllAnnotations();
return;
}
final Map<ErlangProjectionAnnotation, Position> additions = new HashMap<>();
final List<ErlangProjectionAnnotation> deletions = new ArrayList<>();
final List<ErlangProjectionAnnotation> updates = new ArrayList<>();
// use a linked map to maintain ordering of comments
final Map<ErlangProjectionAnnotation, Position> updated = new LinkedHashMap<>();
computeAdditions(fModule, updated);
final Map<Object, List<Tuple>> previous = createAnnotationMap(model);
for (final Entry<ErlangProjectionAnnotation, Position> entry : updated
.entrySet()) {
final ErlangProjectionAnnotation newAnnotation = entry.getKey();
final IErlElement element = newAnnotation.getElement();
final Position newPosition = entry.getValue();
final List<Tuple> annotations = previous.get(element);
if (annotations == null) {
additions.put(newAnnotation, newPosition);
} else {
final Iterator<Tuple> x = annotations.iterator();
boolean matched = false;
while (x.hasNext()) {
final Tuple tuple = x.next();
final ErlangProjectionAnnotation existingAnnotation = tuple.annotation;
final 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);
}
}
}
for (final List<Tuple> l : previous.values()) {
for (final Tuple t : l) {
deletions.add(t.annotation);
}
}
match(deletions, additions, updates);
final Annotation[] removals = new Annotation[deletions.size()];
deletions.toArray(removals);
final Annotation[] changes = new Annotation[updates.size()];
updates.toArray(changes);
model.modifyAnnotations(removals, additions, changes);
fFirstTimeInitialCollapse = false;
} finally {
fCachedDocument = null;
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(final List<ErlangProjectionAnnotation> deletions,
final Map<ErlangProjectionAnnotation, Position> additions,
final List<ErlangProjectionAnnotation> changes) {
if (deletions.isEmpty() || additions.isEmpty() && changes.isEmpty()) {
return;
}
final List<ErlangProjectionAnnotation> newDeletions = new ArrayList<>();
final List<ErlangProjectionAnnotation> newChanges = new ArrayList<>();
final Iterator<ErlangProjectionAnnotation> deletionIterator = deletions
.iterator();
while (deletionIterator.hasNext()) {
final ErlangProjectionAnnotation deleted = deletionIterator.next();
final Position deletedPosition = fCachedModel.getPosition(deleted);
if (deletedPosition == null) {
continue;
}
final 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) {
final IErlElement element = match.annotation.getElement();
deleted.setElement(element);
deletedPosition.setLength(match.position.getLength());
if (deletedPosition instanceof ErlangElementPosition
&& element instanceof IErlMember) {
final ErlangElementPosition eep = (ErlangElementPosition) deletedPosition;
eep.setMember((IErlMember) element);
}
deletionIterator.remove();
newChanges.add(deleted);
if (addToDeletions) {
newDeletions.add(match.annotation);
}
}
}
deletions.addAll(newDeletions);
changes.addAll(newChanges);
}
/**
* Finds a match for <code>tuple</code> in a collection of annotations. The
* positions for the <code>ErlangProjectionAnnotation</code> instances in
* <code>annotations</code> can be found in the passed
* <code>positionMap</code> or <code>fCachedModel</code> if
* <code>positionMap</code> is <code>null</code>.
* <p>
* A tuple is said to match another if their annotations have the same
* comment flag and their position offsets are equal.
* </p>
* <p>
* If a match is found, the annotation gets removed from
* <code>annotations</code>.
* </p>
*
* @param tuple
* the tuple for which we want to find a match
* @param annotations
* collection of <code>ErlangProjectionAnnotation</code>
* @param positionMap
* a <code>Map<Annotation, Position></code> or
* <code>null</code>
* @return a matching tuple or <code>null</code> for no match
*/
private Tuple findMatch(final Tuple tuple,
final Collection<ErlangProjectionAnnotation> annotations,
final Map<ErlangProjectionAnnotation, Position> positionMap) {
final Iterator<ErlangProjectionAnnotation> it = annotations.iterator();
while (it.hasNext()) {
final ErlangProjectionAnnotation annotation = it.next();
if (tuple.annotation.isComment() == annotation.isComment()) {
final Position position = positionMap == null ? fCachedModel
.getPosition(annotation) : positionMap.get(annotation);
if (position == null) {
continue;
}
if (tuple.position.getOffset() == position.getOffset()) {
it.remove();
return new Tuple(annotation, position);
}
}
}
return null;
}
private Map<Object, List<Tuple>> createAnnotationMap(final IAnnotationModel model) {
final Map<Object, List<Tuple>> map = new HashMap<>();
final Iterator<?> e = model.getAnnotationIterator();
while (e.hasNext()) {
final Object annotation = e.next();
if (annotation instanceof ErlangProjectionAnnotation) {
final ErlangProjectionAnnotation epa = (ErlangProjectionAnnotation) annotation;
final Position position = model.getPosition(epa);
List<Tuple> list = map.get(epa.getElement());
if (list == null) {
list = new ArrayList<>(2);
map.put(epa.getElement(), list);
}
list.add(new Tuple(epa, position));
}
}
final Comparator<Tuple> comparator = new Comparator<Tuple>() {
@Override
public int compare(final Tuple o1, final Tuple o2) {
return o1.position.getOffset() - o2.position.getOffset();
}
};
for (final List<Tuple> name : map.values()) {
final List<Tuple> list = name;
Collections.sort(list, comparator);
}
return map;
}
@Override
public void collapseFunctions() {
modifyFiltered(fCollapseFunctionsFilter, false);
}
@Override
public void collapseComments() {
modifyFiltered(fCollapseCommentsFilter, false);
}
@Override
public void expandAll() {
modifyFiltered(fExpandAllFilter, true);
}
@Override
public void collapseElements(final IErlElement[] elements) {
final Set<IErlElement> set = new HashSet<>(Arrays.asList(elements));
modifyFiltered(new ErlangElementSetFilter(set, false), false);
}
@Override
public void expandElements(final IErlElement[] elements) {
final Set<IErlElement> set = new HashSet<>(Arrays.asList(elements));
modifyFiltered(new ErlangElementSetFilter(set, true), true);
}
/**
* Collapses all annotations matched by the passed filter.
*
* @param filter
* the filter to use to select which annotations to collapse
* @param expand
* <code>true</code> to expand the matched annotations,
* <code>false</code> to collapse them
* @since 3.2
*/
private void modifyFiltered(final Filter filter, final boolean expand) {
if (!isInstalled()) {
return;
}
final ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor
.getAdapter(ProjectionAnnotationModel.class);
if (model == null) {
return;
}
final List<ErlangProjectionAnnotation> modified = new ArrayList<>();
final Iterator<?> iter = model.getAnnotationIterator();
while (iter.hasNext()) {
final Object annotation = iter.next();
if (annotation instanceof ErlangProjectionAnnotation) {
final ErlangProjectionAnnotation epa = (ErlangProjectionAnnotation) annotation;
if (filter.match(epa)) {
if (expand) {
epa.markExpanded();
} else {
epa.markCollapsed();
}
modified.add(epa);
}
}
}
model.modifyAnnotations(null, null,
modified.toArray(new Annotation[modified.size()]));
}
@Override
public void elementChanged(final IErlElement element) {
if (fEditor == null) {
return;
}
final IDocumentProvider provider = fEditor.getDocumentProvider();
if (provider == null) {
return;
}
final IEditorInput input = fEditor.getEditorInput();
if (input == null) {
return;
}
fCachedDocument = provider.getDocument(input);
if (fCachedDocument == null) {
return;
}
// fFirstType= null;
// fHasHeaderComment = false;
try {
final ProjectionAnnotationModel model = (ProjectionAnnotationModel) fEditor
.getAdapter(ProjectionAnnotationModel.class);
if (model == null) {
return;
}
fCachedModel = model;
if (element instanceof IErlModule && element != fModule) {
return;
}
final IErlElementDelta d = ErlangEngine
.getInstance()
.getModel()
.createElementDelta(IErlElementDelta.CHANGED,
IErlElementDelta.F_CONTENT, fModule);
processDelta(d);
} finally {
fCachedDocument = null;
fCachedModel = null;
}
}
}