Package com.dubture.twig.ui.editor.folding

Source Code of com.dubture.twig.ui.editor.folding.StructuredTextFoldingProviderTwig

/*******************************************************************************
* This file is part of the Twig eclipse plugin.
*
* (c) Robert Gruendler <r.gruendler@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
******************************************************************************/
package com.dubture.twig.ui.editor.folding;

import java.util.ArrayList;
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.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.compiler.problem.DefaultProblem;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.ElementChangedEvent;
import org.eclipse.dltk.core.IElementChangedListener;
import org.eclipse.dltk.core.IMember;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IModelElementDelta;
import org.eclipse.dltk.core.IParent;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.ISourceReference;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.corext.SourceRange;
import org.eclipse.dltk.internal.ui.editor.EditorUtility;
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.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.php.internal.ui.PHPUiPlugin;
import org.eclipse.php.internal.ui.editor.PHPStructuredEditor;
import org.eclipse.php.internal.ui.editor.PHPStructuredTextViewer;
import org.eclipse.php.internal.ui.editor.configuration.PHPStructuredTextViewerConfiguration;
import org.eclipse.php.internal.ui.folding.IStructuredTextFoldingProvider;
import org.eclipse.php.internal.ui.folding.StructuredTextFoldingProviderPHP;
import org.eclipse.php.internal.ui.folding.html.ProjectionModelNodeAdapterFactoryHTML;
import org.eclipse.php.internal.ui.folding.html.ProjectionModelNodeAdapterHTML;
import org.eclipse.php.internal.ui.preferences.PreferenceConstants;
import org.eclipse.php.internal.ui.text.DocumentCharacterIterator;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.PropagatingAdapter;
import org.eclipse.wst.sse.core.internal.model.FactoryRegistry;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Node;

import com.dubture.twig.core.documentModel.parser.TwigSourceElementRequestorExtension;
import com.dubture.twig.core.documentModel.parser.regions.ITwigScriptRegion;
import com.dubture.twig.core.documentModel.parser.regions.TwigRegionTypes;

/**
*
* A folding structured provider based on
* {@link StructuredTextFoldingProviderPHP} to work with twig.
*
*
* Q: How is folding implemented? A: To enable folding for Twig structures like
* {% block %} {% endblock %}, the {@link TwigSourceElementRequestorExtension}
* parses twig sources and adds those structures as "PHP Methods" to the php
* engine.
*
* This way, we can use the built-in PDT folding logic and don't have to provide
* our own hints on where folding starts and end.
*
*
*
*
* @author Robert Gruendler <r.gruendler@gmail.com>
*
*/
@SuppressWarnings("restriction")
public class StructuredTextFoldingProviderTwig implements IProjectionListener,
        IStructuredTextFoldingProvider
{

    private StructuredTextFoldingProviderPHP phpFoldingProvider;
    private ProjectionListener fProjectionListener;
    private PHPStructuredTextViewer viewer;
    private PHPStructuredEditor fEditor;
    private IElementChangedListener fElementListener;
    private volatile int fUpdatingCount = 0;

    private IModelElement fInput;

    /* preferences */
    private boolean fCollapsePhpdoc = true;
    private boolean fCollapseImportContainer = true;
    private boolean fCollapseTypes = true;
    private boolean fCollapseMembers = false;
    private boolean fCollapseHeaderComments = true;

    /**
     * Maximum number of child nodes to add adapters to (limit for performance
     * sake)
     */
    private static final int MAX_CHILDREN = 10;
    /**
     * Maximum number of sibling nodes to add adapters to (limit for performance
     * sake)
     */
    private static final int MAX_SIBLINGS = 1000;

    private IDocument fDocument;

    private interface ICommentScanner
    {

        public abstract void resetTo(int start);

        public abstract int computePreviousComment();

        public abstract int getCurrentCommentStartPosition();

        public abstract int getCurrentCommentEndPosition();

    }

    private static final class TwigElementPosition extends Position implements
            IProjectionPosition
    {

        private IMember fMember;

        public TwigElementPosition(int offset, int length, IMember member)
        {
            super(offset, length);
            Assert.isNotNull(member);
            fMember = member;
        }

        public void setMember(IMember member)
        {
            Assert.isNotNull(member);
            fMember = member;
        }

        /*
         * @seeorg.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 (ModelException 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;
        }

        /*
         * @seeorg.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 (ModelException e) {
                // ignore and use default
            }

            return nameStart - offset;
        }

    }

    private static final class CommentPosition extends Position implements
            IProjectionPosition
    {
        CommentPosition(int offset, int length)
        {
            super(offset, length);
        }

        /*
         * @seeorg.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) {
                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
         * <code>content</code>. Returns 0 if none is found.
         *
         * @param content
         *            the content to search
         * @param prefixEnd
         *            the end of the prefix
         * @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;
        }

        /*
         * @seeorg.eclipse.jface.text.source.projection.IProjectionPosition#
         * computeCaptionOffset(org.eclipse.jface.text.IDocument)
         */
        public int computeCaptionOffset(IDocument document)
        {
            DocumentCharacterIterator sequence = new DocumentCharacterIterator(
                    document, offset, offset + length);
            return findFirstContent(sequence, 0);
        }
    }

    private static final class Tuple
    {
        TwigProjectionAnnotation annotation;
        Position position;

        Tuple(TwigProjectionAnnotation annotation, Position position)
        {
            this.annotation = annotation;
            this.position = position;
        }
    }

    public class CommentScanner implements ICommentScanner
    {

        private final IDocument document;
        private int startElement;
        private int start;
        private int end;

        public CommentScanner(IDocument document)
        {
            if (document == null) {
                throw new IllegalArgumentException();
            }
            this.document = document;
        }

        public void resetTo(int start)
        {
            this.startElement = start;
        }

        public int getCurrentCommentStartPosition()
        {
            return start;
        }

        public int computePreviousComment()
        {
            start = startElement - 1;

            try {
                // ignore whitespaces
                while (start > 0
                        && Character.isWhitespace(document.getChar(start))) {
                    start--;
                }

                if (start > 0 && document.getChar(start--) != '{') {
                    return startElement;
                }
                // remember the end of the comment
                int end = start;

                if (start > 0 && document.getChar(start--) != '#') {
                    return startElement;
                }

                while (start > 0
                        && (document.getChar(start) != '#' || document
                                .getChar(start - 1) != '}')) {
                    start--;
                }

                if (start == 0)
                    return startElement;
                else {
                    this.end = end;
                    return start;
                }

            } catch (BadLocationException e) {
                return startElement;
            }
        }

        public int getCurrentCommentEndPosition()
        {
            return this.end;
        }
    }

    /**
     * A {@link ProjectionAnnotation} for java code.
     */
    protected static final class TwigProjectionAnnotation extends
            ProjectionAnnotation
    {

        private IModelElement fJavaElement;
        private boolean fIsComment;

        /**
         * Creates a new projection annotation.
         *
         * @param isCollapsed
         *            <code>true</code> to set the initial state to collapsed,
         *            <code>false</code> to set it to expanded
         * @param element
         *            the java element this annotation refers to
         * @param isComment
         *            <code>true</code> for a foldable comment,
         *            <code>false</code> for a foldable code element
         */
        public TwigProjectionAnnotation(boolean isCollapsed,
                IModelElement element, boolean isComment)
        {
            super(isCollapsed);
            fJavaElement = element;
            fIsComment = isComment;
        }

        IModelElement getElement()
        {
            return fJavaElement;
        }

        void setElement(IModelElement element)
        {
            fJavaElement = element;
        }

        boolean isComment()
        {
            return fIsComment;
        }

        void setIsComment(boolean isComment)
        {
            fIsComment = isComment;
        }

        /*
         * @see java.lang.Object#toString()
         */
        public String toString()
        {
            return "TwigProjectionAnnotation:\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" + isComment() + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
        }
    }

    /**
     * A context that contains the information needed to compute the folding
     * structure of an {@link ICompilationUnit} or an {@link IClassFile}.
     * Computed folding regions are collected via
     * {@linkplain #addProjectionRange(StructuredTextFoldingProviderPHP.TwigProjectionAnnotation, Position)
     * addProjectionRange}.
     */
    protected final class FoldingStructureComputationContext
    {
        private final ProjectionAnnotationModel fModel;
        private final IDocument fDocument;

        private final boolean fAllowCollapsing;

        private IModelElement fFirstElement;
        private ICommentScanner fScanner;
        private boolean fHasHeaderComment;
        private LinkedHashMap<Object, Position> fMap = new LinkedHashMap<Object, Position>();

        private boolean headerChecked = false;

        private FoldingStructureComputationContext(IDocument document,
                ProjectionAnnotationModel model, boolean allowCollapsing)
        {
            Assert.isNotNull(document);
            Assert.isNotNull(model);
            fDocument = document;
            fModel = model;
            fAllowCollapsing = allowCollapsing;
        }

        private void setFirstElement(IModelElement element)
        {
            if (hasFirstElement())
                throw new IllegalStateException();
            fFirstElement = element;
        }

        boolean hasFirstElement()
        {
            return fFirstElement != null;
        }

        private IModelElement getFirstElement()
        {
            return fFirstElement;
        }

        private boolean hasHeaderComment()
        {
            return fHasHeaderComment;
        }

        private ICommentScanner getScanner()
        {
            if (fScanner == null)
                fScanner = new CommentScanner(fDocument);
            return fScanner;
        }

        private void setHasHeaderComment()
        {
            fHasHeaderComment = true;
        }

        /**
         * Returns <code>true</code> if newly created folding regions may be
         * collapsed, <code>false</code> if not. This is usually
         * <code>false</code> when updating the folding structure while typing;
         * it may be <code>true</code> when computing or restoring the initial
         * folding structure.
         *
         * @return <code>true</code> if newly created folding regions may be
         *         collapsed, <code>false</code> if not
         */
        public boolean allowCollapsing()
        {
            return fAllowCollapsing;
        }

        /**
         * Returns the document which contains the code being folded.
         *
         * @return the document which contains the code being folded
         */
        private IDocument getDocument()
        {
            return fDocument;
        }

        private ProjectionAnnotationModel getModel()
        {
            return fModel;
        }

        /**
         * Adds a projection (folding) region to this context. The created
         * annotation / position pair will be added to the
         * {@link ProjectionAnnotationModel} of the {@link ProjectionViewer} of
         * the editor.
         *
         * @param annotation
         *            the annotation to add
         * @param position
         *            the corresponding position
         */
        public void addProjectionRange(TwigProjectionAnnotation annotation,
                Position position)
        {
            fMap.put(annotation, position);
        }

        /**
         * Returns <code>true</code> if header comments should be collapsed.
         *
         * @return <code>true</code> if header comments should be collapsed
         */
        public boolean collapseHeaderComments()
        {
            return fAllowCollapsing && fCollapseHeaderComments;
        }

        /**
         * Returns <code>true</code> if import containers should be collapsed.
         *
         * @return <code>true</code> if import containers should be collapsed
         */
        public boolean collapseImportContainer()
        {
            return fAllowCollapsing && fCollapseImportContainer;
        }

        /**
         * Returns <code>true</code> if inner types should be collapsed.
         *
         * @return <code>true</code> if inner types should be collapsed
         */
        public boolean collapseTypes()
        {
            return fAllowCollapsing && fCollapseTypes;
        }

        /**
         * Returns <code>true</code> if javadoc comments should be collapsed.
         *
         * @return <code>true</code> if javadoc comments should be collapsed
         */
        public boolean collapseJavadoc()
        {
            return fAllowCollapsing && fCollapsePhpdoc;
        }

        /**
         * Returns <code>true</code> if methods should be collapsed.
         *
         * @return <code>true</code> if methods should be collapsed
         */
        public boolean collapseMembers()
        {
            return fAllowCollapsing && fCollapseMembers;
        }

        /**
         * @return true if the header was computed already
         */
        public boolean isHeaderChecked()
        {
            return headerChecked;
        }

        /**
         * set the header checked property
         */
        public void setHeaderChecked()
        {
            headerChecked = true;
        }

    }

    /**
     * Internal projection listener.
     */
    private final class ProjectionListener implements IProjectionListener
    {
        private ProjectionViewer fViewer;

        /**
         * Registers the listener with the viewer.
         *
         * @param viewer
         *            the viewer to register a listener with
         */
        public ProjectionListener(ProjectionViewer viewer)
        {
            Assert.isLegal(viewer != null);
            fViewer = viewer;
            fViewer.addProjectionListener(this);
        }

        /**
         * Disposes of this listener and removes the projection listener from
         * the viewer.
         */
        public void dispose()
        {
            if (fViewer != null) {
                fViewer.removeProjectionListener(this);
                fViewer = null;
            }
        }

        /*
         * @seeorg.eclipse.jface.text.source.projection.IProjectionListener#
         * projectionEnabled()
         */
        public void projectionEnabled()
        {
            handleProjectionEnabled();
        }

        /*
         * @seeorg.eclipse.jface.text.source.projection.IProjectionListener#
         * projectionDisabled()
         */
        public void projectionDisabled()
        {
            handleProjectionDisabled();
        }
    }

    @Override
    public void install(ProjectionViewer viewer)
    {

        internalUninstall();

        PHPStructuredTextViewer viewer1 = (PHPStructuredTextViewer) viewer;
        ITextEditor editor = viewer1.getTextEditor();

        this.viewer = viewer1;

        if (editor instanceof PHPStructuredEditor) {

            PHPStructuredEditor phpEditor = (PHPStructuredEditor) editor;

            if (phpEditor.getSourceViwerConfiguration().getClass() == PHPStructuredTextViewerConfiguration.class) {
                phpFoldingProvider = new StructuredTextFoldingProviderPHP();
                phpFoldingProvider.install(viewer);
            } else {

                fProjectionListener = new ProjectionListener(viewer);
                fEditor = (PHPStructuredEditor) editor;
            }
        }
    }

    private void internalUninstall()
    {
        if (isInstalled()) {
            handleProjectionDisabled();
            fProjectionListener.dispose();
            fProjectionListener = null;
            fEditor = null;
        }
    }

    @Override
    public void uninstall()
    {

        if (phpFoldingProvider != null) {
            phpFoldingProvider.uninstall();
            return;
        }

        internalUninstall();

    }

    @Override
    public void initialize()
    {

        if (phpFoldingProvider != null) {
            phpFoldingProvider.initialize();
            return;
        }

        if (viewer != null) {
            fDocument = viewer.getDocument();

            // set projection viewer on new document's adapter factory
            if (viewer.getProjectionAnnotationModel() != null) {
                final ProjectionModelNodeAdapterFactoryHTML factory2 = getAdapterFactoryHTML(true);
                if (factory2 != null) {
                    factory2.addProjectionViewer(viewer);
                }

                addAllAdapters();
            }
        }

        fUpdatingCount++;
        try {
            update(createInitialContext());
        } finally {
            fUpdatingCount--;
        }

    }

    @SuppressWarnings("rawtypes")
    private void update(FoldingStructureComputationContext ctx)
    {

        Map<TwigProjectionAnnotation, Position> additions = new HashMap<TwigProjectionAnnotation, Position>();
        List<TwigProjectionAnnotation> deletions = new ArrayList<TwigProjectionAnnotation>();
        List<TwigProjectionAnnotation> updates = new ArrayList<TwigProjectionAnnotation>();

        computeFoldingStructure(ctx);
        Map<Object, Position> newStructure = ctx.fMap;
        Map<IModelElement, Object> oldStructure = computeCurrentStructure(ctx);

        Iterator<Object> e = newStructure.keySet().iterator();

        while (e.hasNext()) {
            TwigProjectionAnnotation newAnnotation = (TwigProjectionAnnotation) e
                    .next();
            Position newPosition = newStructure.get(newAnnotation);

            IModelElement element = newAnnotation.getElement();
            /*
             * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=130472 and
             * https://bugs.eclipse.org/bugs/show_bug.cgi?id=127445 In the
             * presence of syntax errors, anonymous types may have a source
             * range offset of 0. When such a situation is encountered, we
             * ignore the proposed folding range: if no corresponding folding
             * range exists, it is silently ignored; if there *is* a matching
             * folding range, we ignore the position update and keep the old
             * range, in order to keep the folding structure stable.
             */
            boolean isMalformedAnonymousType = newPosition.getOffset() == 0
                    && element.getElementType() == IModelElement.TYPE;
            List annotations = (List) oldStructure.get(element);
            if (annotations == null) {
                if (!isMalformedAnonymousType)
                    additions.put(newAnnotation, newPosition);
            } else {
                Iterator x = annotations.iterator();
                boolean matched = false;
                while (x.hasNext()) {
                    Tuple tuple = (Tuple) x.next();
                    TwigProjectionAnnotation existingAnnotation = tuple.annotation;
                    Position existingPosition = tuple.position;
                    if (newAnnotation.isComment() == existingAnnotation
                            .isComment()) {
                        boolean updateCollapsedState = ctx.allowCollapsing()
                                && existingAnnotation.isCollapsed() != newAnnotation
                                        .isCollapsed();
                        if (!isMalformedAnonymousType
                                && existingPosition != null
                                && (!newPosition.equals(existingPosition) || updateCollapsedState)) {
                            existingPosition.setOffset(newPosition.getOffset());
                            existingPosition.setLength(newPosition.getLength());
                            if (updateCollapsedState)
                                if (newAnnotation.isCollapsed())
                                    existingAnnotation.markCollapsed();
                                else
                                    existingAnnotation.markExpanded();
                            updates.add(existingAnnotation);
                        }
                        matched = true;
                        x.remove();
                        break;
                    }
                }
                if (!matched)
                    additions.put(newAnnotation, newPosition);

                if (annotations.isEmpty())
                    oldStructure.remove(element);
            }
        }

        e = oldStructure.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, ctx);

        Annotation[] deletedArray = deletions.toArray(new Annotation[deletions
                .size()]);
        Annotation[] changedArray = updates.toArray(new Annotation[updates
                .size()]);
        ctx.getModel().modifyAnnotations(deletedArray, additions, changedArray);

    }

    private void computeFoldingStructure(FoldingStructureComputationContext ctx)
    {
        IParent parent = (IParent) fInput;
        try {
            if (!(fInput instanceof ISourceReference))
                return;
            computeFoldingStructure(parent.getChildren(), ctx);
        } catch (ModelException x) {
        }
    }

    private void computeFoldingStructure(IModelElement[] elements,
            FoldingStructureComputationContext ctx) throws ModelException
    {
        for (int i = 0; i < elements.length; i++) {
            IModelElement element = elements[i];

            computeFoldingStructure(element, ctx);

            if (element instanceof IParent) {
                IParent parent = (IParent) element;
                computeFoldingStructure(parent.getChildren(), ctx);
            }
        }
    }

    protected void computeFoldingStructure(IModelElement element,
            FoldingStructureComputationContext ctx)
    {

        boolean collapse = false;
        boolean collapseCode = true;

        switch (element.getElementType()) {

        // TODO : ask DLTK to have include container
            case IModelElement.IMPORT_CONTAINER :
                collapse = ctx.collapseImportContainer();
                break;
            case IModelElement.TYPE :
                collapse = ctx.collapseTypes();
                break;
            case IModelElement.METHOD :
                collapse = ctx.collapseMembers();
                break;
            case IModelElement.FIELD :
                // class fields should be folded as well
                IModelElement parent = element.getParent();
                if (parent != null
                        && parent.getElementType() != IModelElement.TYPE) {
                    return;
                }
                collapse = ctx.collapseMembers();
                break;
            default :
                return;
        }

        IRegion[] regions = computeProjectionRanges((ISourceReference) element,
                ctx);
        if (regions.length > 0) {
            // comments
            // use the set to filter the duplicate comment IRegion,or sometimes
            // you will see the header comment is collapsed but with a expanded
            // iamge,this is because there are two ProjectionAnnotations for one
            // comment,and the second ProjectionAnnotation's image overide the
            // header one
            Set<IRegion> regionSet = new HashSet<IRegion>();
            for (int i = 0; i < regions.length - 1; i++) {
                IRegion normalized = alignRegion(regions[i], ctx);
                if (normalized != null && regionSet.add(normalized)) {
                    Position position = createCommentPosition(normalized);
                    if (position != null) {
                        boolean commentCollapse;
                        if (i == 0
                                && (regions.length > 2 || ctx
                                        .hasHeaderComment())
                                && element == ctx.getFirstElement()) {
                            commentCollapse = ctx.collapseHeaderComments();
                        } else {
                            commentCollapse = ctx.collapseJavadoc();
                        }
                        ctx.addProjectionRange(new TwigProjectionAnnotation(
                                commentCollapse, element, true), position);
                    }
                }
            }
            // code
            if (collapseCode) {
                IRegion normalized = alignRegion(regions[regions.length - 1],
                        ctx);
                if (normalized != null) {
                    Position position = element instanceof IMember
                            ? createMemberPosition(normalized,
                                    (IMember) element)
                            : createCommentPosition(normalized);
                    if (position != null)
                        ctx.addProjectionRange(new TwigProjectionAnnotation(
                                collapse, element, false), position);
                }
            }
        }
    }

    protected final Position createCommentPosition(IRegion aligned)
    {
        return new CommentPosition(aligned.getOffset(), aligned.getLength());
    }

    protected final Position createMemberPosition(IRegion aligned,
            IMember member)
    {
        return new TwigElementPosition(aligned.getOffset(),
                aligned.getLength(), member);
    }

    protected final IRegion[] computeProjectionRanges(
            ISourceReference reference, FoldingStructureComputationContext ctx)
    {
        try {
            ISourceRange range = reference.getSourceRange();
            if (!SourceRange.isAvailable(range))
                return new IRegion[0];
            List<IRegion> regions = new ArrayList<IRegion>();
            if (!ctx.isHeaderChecked() && reference instanceof IModelElement) {
                ctx.setFirstElement((IModelElement) reference);
                ctx.setHeaderChecked();

                IRegion headerComment = computeHeaderComment(ctx);
                if (headerComment != null) {
                    regions.add(headerComment);
                    ctx.setHasHeaderComment();
                }
            }

            final int shift = range.getOffset();
            ICommentScanner scanner = ctx.getScanner();
            scanner.resetTo(shift);

            int start = shift;

            start = scanner.computePreviousComment();

            if (start != shift) {
                start = scanner.getCurrentCommentStartPosition();
                int end = scanner.getCurrentCommentEndPosition();
                regions.add(new Region(start, end - start));
            }

            // shift -> start
            regions.add(new Region(shift, range.getLength()));

            IRegion[] result = new IRegion[regions.size()];
            regions.toArray(result);
            return result;
        } catch (ModelException e) {
        }

        return new IRegion[0];
    }

    protected final IRegion alignRegion(IRegion region,
            FoldingStructureComputationContext ctx)
    {
        if (region == null)
            return null;

        IDocument document = ctx.getDocument();

        try {

            int start = document.getLineOfOffset(region.getOffset());
            int end = document.getLineOfOffset(Math.min(region.getOffset()
                    + region.getLength() - 1, document.getLength()));
            if (start >= end)
                return null;

            int offset = document.getLineOffset(start);
            int endOffset;
            if (document.getNumberOfLines() > end + 1)
                endOffset = document.getLineOffset(end + 1);
            else
                endOffset = document.getLineOffset(end)
                        + document.getLineLength(end);

            return new Region(offset, endOffset - offset);

        } catch (BadLocationException x) {
            // concurrent modification
            return null;
        }
    }

    private void match(List<TwigProjectionAnnotation> deletions,
            Map<TwigProjectionAnnotation, Position> additions,
            List<TwigProjectionAnnotation> changes,
            FoldingStructureComputationContext ctx)
    {
        if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty()))
            return;

        List<TwigProjectionAnnotation> newDeletions = new ArrayList<TwigProjectionAnnotation>();
        List<TwigProjectionAnnotation> newChanges = new ArrayList<TwigProjectionAnnotation>();

        Iterator<TwigProjectionAnnotation> deletionIterator = deletions
                .iterator();
        while (deletionIterator.hasNext()) {
            TwigProjectionAnnotation deleted = deletionIterator.next();
            Position deletedPosition = ctx.getModel().getPosition(deleted);
            if (deletedPosition == null)
                continue;

            Tuple deletedTuple = new Tuple(deleted, deletedPosition);

            Tuple match = findMatch(deletedTuple, changes, null, ctx);
            boolean addToDeletions = true;
            if (match == null) {
                match = findMatch(deletedTuple, additions.keySet(), additions,
                        ctx);
                addToDeletions = false;
            }

            if (match != null) {
                IModelElement element = match.annotation.getElement();
                deleted.setElement(element);
                deletedPosition.setLength(match.position.getLength());
                if (deletedPosition instanceof TwigElementPosition
                        && element instanceof IMember) {
                    TwigElementPosition jep = (TwigElementPosition) deletedPosition;
                    jep.setMember((IMember) element);
                }

                deletionIterator.remove();
                newChanges.add(deleted);

                if (addToDeletions)
                    newDeletions.add(match.annotation);
            }
        }

        deletions.addAll(newDeletions);
        changes.addAll(newChanges);
    }

    private Tuple findMatch(Tuple tuple,
            Collection<TwigProjectionAnnotation> annotations,
            Map<TwigProjectionAnnotation, Position> positionMap,
            FoldingStructureComputationContext ctx)
    {
        Iterator<TwigProjectionAnnotation> it = annotations.iterator();
        while (it.hasNext()) {
            TwigProjectionAnnotation annotation = it.next();
            if (tuple.annotation.isComment() == annotation.isComment()) {
                Position position = positionMap == null ? ctx.getModel()
                        .getPosition(annotation) : positionMap.get(annotation);
                if (position == null)
                    continue;

                if (tuple.position.getOffset() == position.getOffset()) {
                    it.remove();
                    return new Tuple(annotation, position);
                }
            }
        }

        return null;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private Map<IModelElement, Object> computeCurrentStructure(
            FoldingStructureComputationContext ctx)
    {
        Map<IModelElement, Object> map = new HashMap<IModelElement, Object>();
        ProjectionAnnotationModel model = ctx.getModel();
        Iterator e = model.getAnnotationIterator();

        while (e.hasNext()) {

            Object annotation = e.next();
            if (annotation instanceof TwigProjectionAnnotation) {
                TwigProjectionAnnotation java = (TwigProjectionAnnotation) annotation;
                Position position = model.getPosition(java);
                Assert.isNotNull(position);
                List<Tuple> list = (List<Tuple>) map.get(java.getElement());
                if (list == null) {
                    list = new ArrayList<Tuple>(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<Object> it = map.values().iterator(); it.hasNext();) {
            List list = (List) it.next();
            Collections.sort(list, comparator);
        }
        return map;
    }

    private IRegion computeHeaderComment(FoldingStructureComputationContext ctx)
            throws ModelException
    {
        if (!(ctx.getDocument() instanceof IStructuredDocument)) {
            return null;
        }
        final IStructuredDocument document = (IStructuredDocument) ctx
                .getDocument();
        IStructuredDocumentRegion sdRegion = document
                .getFirstStructuredDocumentRegion();
        int i = 0;
        while (sdRegion != null
                && sdRegion.getType() != TwigRegionTypes.TWIG_CONTENT
                && i++ < 40) {
            sdRegion = sdRegion.getNext();
        }

        if (sdRegion == null
                || sdRegion.getType() != TwigRegionTypes.TWIG_CONTENT
                || sdRegion.getRegions().size() < 2) {
            return null;
        }

        final ITwigScriptRegion textRegion = (ITwigScriptRegion) sdRegion
                .getRegions().get(1);
        try {
            ITextRegion phpToken = textRegion.getTwigToken(0);
            i = 0;
            while (phpToken != null
                    && phpToken.getType() != /*
                                              * PHPRegionTypes.PHPDOC_COMMENT_START
                                              */TwigRegionTypes.TWIG_COMMENT_TEXT
                    && i++ < 3) {
                phpToken = textRegion.getTwigToken(phpToken.getEnd() + 1);
            }
            if (phpToken == null
                    || phpToken.getType() != /*
                                              * PHPRegionTypes.PHPDOC_COMMENT_START
                                              */TwigRegionTypes.TWIG_COMMENT_TEXT) {
                return null;
            }
            int start = phpToken.getStart();
            ITextRegion lastToken = null;
            while (lastToken != phpToken
                    && phpToken != null
                    && phpToken.getType() != /*
                                              * PHPRegionTypes.PHPDOC_COMMENT_END
                                              */TwigRegionTypes.TWIG_COMMENT_TEXT) {
                phpToken = textRegion.getTwigToken(phpToken.getEnd() + 1);
            }

            if (phpToken != null
                    && phpToken.getType() == /*
                                              * PHPRegionTypes.PHPDOC_COMMENT_END
                                              */TwigRegionTypes.TWIG_COMMENT_TEXT) {
                int end = phpToken.getEnd();
                return new Region(sdRegion.getStartOffset()
                        + textRegion.getStart() + start, end - start);
            }
            return null;

        } catch (BadLocationException e) {
            return null;
        }
    }

    private ProjectionAnnotationModel getModel()
    {

        return this.viewer.getProjectionAnnotationModel();
    }

    protected final boolean isInstalled()
    {
        return fEditor != null;
    }

    private FoldingStructureComputationContext createInitialContext()
    {
        initializePreferences();
        fInput = getInputElement();
        if (fInput == null)
            return null;

        return createContext(true);
    }

    private FoldingStructureComputationContext createContext(
            boolean allowCollapse)
    {
        if (!isInstalled())
            return null;
        ProjectionAnnotationModel model = getModel();
        if (model == null)
            return null;
        IDocument doc = getDocument();
        if (doc == null)
            return null;

        return new FoldingStructureComputationContext(doc, model, allowCollapse);
    }

    private IDocument getDocument()
    {
        PHPStructuredEditor editor = fEditor;
        if (editor == null)
            return null;

        IDocumentProvider provider = editor.getDocumentProvider();
        if (provider == null)
            return null;

        return provider.getDocument(editor.getEditorInput());
    }

    private void initializePreferences()
    {
        IPreferenceStore store = PHPUiPlugin.getDefault().getPreferenceStore();
        // fCollapseImportContainer=
        // store.getBoolean(PreferenceConstants.EDITOR_FOLDING_IMPORTS);
        fCollapseTypes = store
                .getBoolean(PreferenceConstants.EDITOR_FOLDING_CLASSES);
        fCollapsePhpdoc = store
                .getBoolean(PreferenceConstants.EDITOR_FOLDING_PHPDOC);
        fCollapseMembers = store
                .getBoolean(PreferenceConstants.EDITOR_FOLDING_FUNCTIONS);
        fCollapseHeaderComments = store
                .getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADER_COMMENTS);
    }

    private IModelElement getInputElement()
    {
        if (fEditor == null)
            return null;
        return EditorUtility.getEditorInputModelElement(fEditor, false);
    }

    public int getUpdatingCount() {
        return fUpdatingCount;
    }

    @Override
    public void projectionEnabled()
    {

        if (phpFoldingProvider != null) {
            phpFoldingProvider.projectionEnabled();
            return;
        }

        handleProjectionEnabled();

    }

    @Override
    public void projectionDisabled()
    {

        if (phpFoldingProvider != null) {
            phpFoldingProvider.projectionDisabled();
            return;
        }

        handleProjectionDisabled();
    }

    protected void handleProjectionEnabled()
    {

        handleProjectionDisabled();

        if (isInstalled()) {
            initialize();
            fElementListener = new ElementChangedListener();
            DLTKCore.addElementChangedListener(fElementListener);
        }
    }

    /**
     * Called whenever projection is disabled, for example when the provider is
     * {@link #uninstall() uninstalled}, when the viewer issues a
     * {@link IProjectionListener#projectionDisabled() projectionDisabled}
     * message and before {@link #handleProjectionEnabled() enabling} the
     * provider. Implementations must be prepared to handle multiple calls to
     * this method even if the provider is already disabled.
     * <p>
     * Subclasses may extend.
     * </p>
     */
    protected void handleProjectionDisabled()
    {

        if (fElementListener != null) {
            DLTKCore.removeElementChangedListener(fElementListener);
            fElementListener = null;
        }

        final ProjectionModelNodeAdapterFactoryHTML factory2 = getAdapterFactoryHTML(false);
        if (factory2 != null) {
            factory2.removeProjectionViewer(viewer);
        }

        // clear out all annotations
        if (viewer.getProjectionAnnotationModel() != null) {
            viewer.getProjectionAnnotationModel().removeAllAnnotations();
        }

        removeAllAdapters();

        fDocument = null;
    }

    private ProjectionModelNodeAdapterFactoryHTML getAdapterFactoryHTML(
            boolean createIfNeeded)
    {
        ProjectionModelNodeAdapterFactoryHTML factory = null;
        if (fDocument != null) {
            IStructuredModel sModel = null;
            try {
                sModel = StructuredModelManager.getModelManager()
                        .getExistingModelForRead(fDocument);
                if (sModel != null && (sModel instanceof IDOMModel)) {
                    final FactoryRegistry factoryRegistry = sModel
                            .getFactoryRegistry();

                    // getting the projectionmodelnodeadapter for the first
                    // time
                    // so do some initializing
                    if (!factoryRegistry
                            .contains(ProjectionModelNodeAdapterHTML.class)
                            && createIfNeeded) {
                        final ProjectionModelNodeAdapterFactoryHTML newFactory = new ProjectionModelNodeAdapterFactoryHTML();

                        // add factory to factory registry
                        factoryRegistry.addFactory(newFactory);

                        // add factory to propogating adapter
                        final IDOMModel domModel = (IDOMModel) sModel;
                        final IDOMDocument document = domModel.getDocument();
                        final PropagatingAdapter propagatingAdapter = (PropagatingAdapter) ((INodeNotifier) document)
                                .getAdapterFor(PropagatingAdapter.class);
                        if (propagatingAdapter != null) {
                            propagatingAdapter
                                    .addAdaptOnCreateFactory(newFactory);
                        }
                    }

                    // try and get the factory
                    factory = (ProjectionModelNodeAdapterFactoryHTML) factoryRegistry
                            .getFactoryFor(ProjectionModelNodeAdapterHTML.class);
                }
            } finally {
                if (sModel != null) {
                    sModel.releaseFromRead();
                }
            }
        }

        return factory;
    }

    private void addAllAdapters()
    {
        if (fDocument != null) {
            IStructuredModel sModel = null;
            try {
                sModel = StructuredModelManager.getModelManager()
                        .getExistingModelForRead(fDocument);
                if (sModel != null) {
                    IndexedRegion startNode = sModel.getIndexedRegion(0);
                    if (startNode == null) {
                        assert sModel instanceof IDOMModel;
                        startNode = ((IDOMModel) sModel).getDocument();
                    }

                    if (startNode instanceof Node) {
                        int siblingLevel = 0;
                        Node nextSibling = (Node) startNode;

                        while (nextSibling != null
                                && siblingLevel < MAX_SIBLINGS) {
                            final Node currentNode = nextSibling;
                            nextSibling = currentNode.getNextSibling();

                            addAdapterToNodeAndChildrenHTML(currentNode, 0);
                            ++siblingLevel;
                        }
                    }
                }
            } finally {
                if (sModel != null) {
                    sModel.releaseFromRead();
                }
            }
        }
    }

    private void addAdapterToNodeAndChildrenHTML(Node node, int childLevel)
    {
        // stop adding initial adapters MAX_CHILDREN levels deep for
        // performance sake
        if (node instanceof INodeNotifier && childLevel < MAX_CHILDREN) {
            final INodeNotifier notifier = (INodeNotifier) node;

            // try and get the adapter for the current node and update the
            // adapter with projection information
            final ProjectionModelNodeAdapterHTML adapter2 = (ProjectionModelNodeAdapterHTML) notifier
                    .getExistingAdapter(ProjectionModelNodeAdapterHTML.class);
            if (adapter2 != null) {
                adapter2.updateAdapter(node);
            } else {
                // just call getadapter so the adapter is created and
                // automatically initialized
                notifier.getAdapterFor(ProjectionModelNodeAdapterHTML.class);
            }
            int siblingLevel = 0;
            Node nextChild = node.getFirstChild();
            while (nextChild != null && siblingLevel < MAX_SIBLINGS) {
                final Node childNode = nextChild;
                nextChild = childNode.getNextSibling();

                addAdapterToNodeAndChildrenHTML(childNode, childLevel + 1);
                ++siblingLevel;
            }
        }
    }

    private void removeAllAdapters()
    {
        if (fDocument != null) {
            IStructuredModel sModel = null;
            try {
                sModel = StructuredModelManager.getModelManager()
                        .getExistingModelForRead(fDocument);
                if (sModel != null) {
                    final int startOffset = 0;
                    final IndexedRegion startNode = sModel
                            .getIndexedRegion(startOffset);
                    if (startNode instanceof Node) {
                        Node nextSibling = (Node) startNode;
                        while (nextSibling != null) {
                            final Node currentNode = nextSibling;
                            nextSibling = currentNode.getNextSibling();

                            removeAdapterFromNodeAndChildren(currentNode, 0);
                        }
                    }
                }
            } finally {
                if (sModel != null) {
                    sModel.releaseFromRead();
                }
            }
        }

    }

    private void removeAdapterFromNodeAndChildren(Node node, int level)
    {
        if (node instanceof INodeNotifier) {
            final INodeNotifier notifier = (INodeNotifier) node;

            // try and get the adapter for the current node and remove it
            final INodeAdapter adapter2 = notifier
                    .getExistingAdapter(ProjectionModelNodeAdapterHTML.class);
            if (adapter2 != null) {
                notifier.removeAdapter(adapter2);
            }

            Node nextChild = node.getFirstChild();
            while (nextChild != null) {
                final Node childNode = nextChild;
                nextChild = childNode.getNextSibling();

                removeAdapterFromNodeAndChildren(childNode, level + 1);
            }
        }
    }

    private class ElementChangedListener implements IElementChangedListener
    {

        /*
         * @see
         * org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse
         * .jdt.core.ElementChangedEvent)
         */
        public void elementChanged(ElementChangedEvent e)
        {
            IModelElementDelta delta = findElement(fInput, e.getDelta());
            if (delta != null
                    && (delta.getFlags() & (IModelElementDelta.F_CONTENT | IModelElementDelta.F_CHILDREN)) != 0) {

                if (shouldIgnoreDelta(e.getDelta().getElement(), delta))
                    return;

                fUpdatingCount++;
                try {
                    update(createContext(false));
                } finally {
                    fUpdatingCount--;
                }
            }
        }

        /**
         * Ignore the delta if there are errors on the caret line.
         * <p>
         * We don't ignore the delta if an import is added and the caret isn't
         * inside the import container.
         * </p>
         *
         * @param ast
         *            the compilation unit AST
         * @param delta
         *            the Java element delta for the given AST element
         * @return <code>true</code> if the delta should be ignored
         * @since 3.3
         */
        private boolean shouldIgnoreDelta(IModelElement ast,
                IModelElementDelta delta)
        {
            if (ast == null)
                return false; // can't compute

            if (!(ast.getResource() instanceof IFile)) {
                return false;
            }
            final IFile resource = (IFile) ast.getResource();

            IDocument document = getDocument();
            if (document == null)
                return false; // can't compute

            PHPStructuredEditor editor = fEditor;
            if (editor == null || editor.getCachedSelectedRange() == null)
                return false; // can't compute

            int caretLine = 0;
            try {
                caretLine = document.getLineOfOffset(editor
                        .getCachedSelectedRange().x) + 1;
            } catch (BadLocationException x) {
                return false; // can't compute
            }

            if (caretLine > 0) {
                try {
                    IMarker[] problems = resource.findMarkers(
                            DefaultProblem.MARKER_TYPE_PROBLEM, false,
                            IResource.DEPTH_INFINITE);
                    for (int i = 0; i < problems.length; i++) {
                        final IMarker marker = problems[i];
                        final boolean isInCaret = isCaretLine(caretLine,
                                IMarker.LINE_NUMBER, marker);
                        final boolean isSyntaxError = isCaretLine(
                                IMarker.SEVERITY_ERROR, IMarker.SEVERITY,
                                marker);
                        if (isSyntaxError && isInCaret) {
                            return true;
                        }
                    }

                } catch (CoreException e) {
                    return false;
                }
            }
            return false;
        }

        private final boolean isCaretLine(int expected, String attribute,
                final IMarker marker) throws CoreException
        {
            final Object res = marker.getAttribute(attribute); // IMarker.LINE_NUMBER
            return res != null && res instanceof Integer
                    && (Integer) res == expected;
        }

        private IModelElementDelta findElement(IModelElement target,
                IModelElementDelta delta)
        {

            if (delta == null || target == null)
                return null;

            IModelElement element = delta.getElement();

            if (element.getElementType() > IModelElement.BINARY_MODULE)
                return null;

            if (target.equals(element))
                return delta;

            IModelElementDelta[] children = delta.getAffectedChildren();

            for (int i = 0; i < children.length; i++) {
                IModelElementDelta d = findElement(target, children[i]);
                if (d != null)
                    return d;
            }

            return null;
        }
    }
}
TOP

Related Classes of com.dubture.twig.ui.editor.folding.StructuredTextFoldingProviderTwig

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.