org.deved.antlride.common.ui.text.folding.AntlrAbstractFoldingStructureProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.deved.antlride.common.ui.text.folding.AntlrAbstractFoldingStructureProvider.java

Source

/*******************************************************************************
 * Copyright (c) 2007, 2008 Edgar Espina.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
     
 *******************************************************************************/

package org.deved.antlride.common.ui.text.folding;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.declarations.MethodDeclaration;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.declarations.TypeDeclaration;
import org.eclipse.dltk.ast.parser.ISourceParser;
import org.eclipse.dltk.compiler.env.ModuleSource;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.DLTKLanguageManager;
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.IModelElementVisitor;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.ISourceReference;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.SourceParserUtil;
import org.eclipse.dltk.corext.SourceRange;
import org.eclipse.dltk.internal.core.SourceMethod;
import org.eclipse.dltk.internal.ui.editor.ScriptEditor;
import org.eclipse.dltk.internal.ui.text.DocumentCharacterIterator;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.PreferenceConstants;
import org.eclipse.dltk.ui.text.folding.AbstractASTFoldingStructureProvider;
import org.eclipse.dltk.ui.text.folding.DefaultElementCommentResolver;
import org.eclipse.dltk.ui.text.folding.IElementCommentResolver;
import org.eclipse.dltk.ui.text.folding.IFoldingStructureProvider;
import org.eclipse.dltk.ui.text.folding.IFoldingStructureProviderExtension;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
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.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * Updates the projection model of a source module using AST info.
 * 
 * Just a copy of {@link AbstractASTFoldingStructureProvider} TODO: delete this
 * class when DLTK supports multiples languages
 */
@SuppressWarnings("restriction")
public abstract class AntlrAbstractFoldingStructureProvider
        implements IFoldingStructureProvider, IFoldingStructureProviderExtension {

    /**
     * A context that contains the information needed to compute the folding
     * structure of an {@link ISourceModule}. Computed folding regions are
     * collected via
     * {@link #addProjectionRange(AbstractASTFoldingStructureProvider.ScriptProjectionAnnotation, Position)
     * addProjectionRange}.
     */
    public static final class FoldingStructureComputationContext {
        private final ProjectionAnnotationModel fModel;
        private final IDocument fDocument;
        private final boolean fAllowCollapsing;
        protected LinkedHashMap<Annotation, Position> fMap = new LinkedHashMap<Annotation, Position>();

        public FoldingStructureComputationContext(IDocument document, ProjectionAnnotationModel model,
                boolean allowCollapsing) {
            fDocument = document;
            fModel = model;
            fAllowCollapsing = allowCollapsing;
        }

        public Map<Annotation, Position> getMap() {
            return fMap;
        }

        /**
         * 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
         */
        IDocument getDocument() {
            return fDocument;
        }

        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(ScriptProjectionAnnotation annotation, Position position) {
            fMap.put(annotation, position);
        }
    }

    protected static final class SourceRangeStamp {
        private int hash, length;

        public SourceRangeStamp(int hash, int lenght) {
            this.hash = hash;
            this.length = lenght;
        }

        /**
         * @return the hash
         */
        public int getHash() {
            return hash;
        }

        /**
         * @param hash
         *            the hash to set
         */
        public void setHash(int hash) {
            this.hash = hash;
        }

        /**
         * @return the length
         */
        public int getLength() {
            return length;
        }

        /**
         * @param length
         *            the length to set
         */
        public void setLength(int length) {
            this.length = length;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof SourceRangeStamp) {
                SourceRangeStamp s = (SourceRangeStamp) obj;
                return (s.hash == hash); // && s.length == length);
            }
            return super.equals(obj);
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return hash;
        }
    }

    /**
     * A {@link ProjectionAnnotation} for code.
     */
    protected static final class ScriptProjectionAnnotation extends ProjectionAnnotation {
        private boolean fIsComment;
        private SourceRangeStamp stamp;
        private IModelElement element;

        /**
         * 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 codeStamp
         *            the stamp of source code this annotation refers to
         * @param isComment
         *            <code>true</code> for a foldable comment,
         *            <code>false</code> for a foldable code element
         */
        public ScriptProjectionAnnotation(boolean isCollapsed, boolean isComment, SourceRangeStamp codeStamp,
                IModelElement element) {
            super(isCollapsed);
            fIsComment = isComment;
            stamp = codeStamp;
            this.element = element;
        }

        public IModelElement getElement() {
            return element;
        }

        boolean isComment() {
            return fIsComment;
        }

        /**
         * @return the stamp
         */
        SourceRangeStamp getStamp() {
            return stamp;
        }

        /**
         * @param stamp
         *            the stamp to set
         */
        void setStamp(SourceRangeStamp stamp) {
            this.stamp = stamp;
        }

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

        /*
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "ScriptProjectionAnnotation:\n" + //$NON-NLS-1$
                    "\tcollapsed: \t" + isCollapsed() + "\n" + //$NON-NLS-1$ //$NON-NLS-2$
                    "\tcomment: \t" + isComment() + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
        }
    }

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

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

    /**
     * Filter for annotations.
     */
    private static interface Filter {
        boolean match(ScriptProjectionAnnotation annotation);
    }

    /**
     * Matches comments.
     */
    private static final class CommentFilter implements Filter {
        public boolean match(ScriptProjectionAnnotation annotation) {
            if (annotation.isComment() && !annotation.isMarkedDeleted()) {
                return true;
            }
            return false;
        }
    }

    /**
     * Matches members.
     */
    private static final class MemberFilter implements Filter {
        public boolean match(ScriptProjectionAnnotation annotation) {
            if (!annotation.isMarkedDeleted() && annotation.getElement() instanceof IMember) {
                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.
     */
    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) {
                // 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
         * <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, 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);
        }
    }

    /**
     * Projection position that will return two foldable regions: one folding
     * away the lines before the one containing the simple name of the script
     * element, one folding away any lines after the caption.
     */
    private static final class ScriptElementPosition extends Position implements IProjectionPosition {
        public ScriptElementPosition(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 {
            int nameStart = offset;
            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) {
            return 0;
        }
    }

    /**
     * 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) {
            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();
        }
    }

    private class ElementChangedListener implements IElementChangedListener {
        /*
         * @see
         * org.eclipse.dltk.core.IElementChangedListener#elementChanged(org.
         * eclipse.dltk.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)
                update(createContext(false));
        }

        private IModelElementDelta findElement(IModelElement target, IModelElementDelta delta) {
            if (delta == null || target == null)
                return null;
            IModelElement element = delta.getElement();
            if (element.getElementType() > IModelElement.SOURCE_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;
        }
    }

    /* context and listeners */
    private ITextEditor fEditor;
    private ProjectionListener fProjectionListener;
    private IModelElement fInput;
    private IElementChangedListener fElementListener;
    /* filters */
    /** Member filter, matches nested members (but not top-level types). */
    private final Filter fMemberFilter = new MemberFilter();
    /** Comment filter, matches comments. */
    private final Filter fCommentFilter = new CommentFilter();
    private IPreferenceStore fStore;

    private int fBlockLinesMin;

    protected boolean fDocsFolding;
    protected boolean fCommentsFolding;
    protected boolean fFoldNewLines;

    protected boolean fInitCollapseComments;
    protected boolean fInitCollapseHeaderComments;
    protected boolean fInitCollapseClasses;
    protected boolean fInitCollapseMethods;

    private boolean fInitCollapseDocs;

    /**
     * Creates a new folding provider. It must be
     * {@link #install(ITextEditor, ProjectionViewer, IPreferenceStore)
     * installed} on an editor/viewer pair before it can be used, and
     * {@link #uninstall() uninstalled} when not used any longer.
     * <p>
     * The projection state may be reset by calling {@link #initialize()}.
     * </p>
     */
    public AntlrAbstractFoldingStructureProvider() {
        // empty constructor
    }

    /**
     * {@inheritDoc}
     * <p>
     * Subclasses may extend.
     * </p>
     * 
     * @param editor
     *            {@inheritDoc}
     * @param viewer
     *            {@inheritDoc}
     */
    public void install(ITextEditor editor, ProjectionViewer viewer, IPreferenceStore store) {
        internalUninstall();
        fStore = store;
        if (editor instanceof ScriptEditor) {
            fEditor = editor;
            fProjectionListener = new ProjectionListener(viewer);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Subclasses may extend.
     * </p>
     */
    public void uninstall() {
        internalUninstall();
    }

    /**
     * Internal implementation of {@link #uninstall()}.
     */
    private void internalUninstall() {
        if (isInstalled()) {
            handleProjectionDisabled();
            fProjectionListener.dispose();
            fProjectionListener = null;
            fEditor = null;
        }
    }

    /**
     * Returns <code>true</code> if the provider is installed,
     * <code>false</code> otherwise.
     * 
     * @return <code>true</code> if the provider is installed,
     *         <code>false</code> otherwise
     */
    protected final boolean isInstalled() {
        return fEditor != null;
    }

    /**
     * Called whenever projection is enabled, for example when the viewer issues
     * a {@link IProjectionListener#projectionEnabled() projectionEnabled}
     * message. When the provider is already enabled when this method is called,
     * it is first {@link #handleProjectionDisabled() disabled}.
     * <p>
     * Subclasses may extend.
     * </p>
     */
    protected void handleProjectionEnabled() {
        handleProjectionDisabled();
        if (fEditor instanceof ScriptEditor) {
            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;
        }
    }

    public final void initialize() {
        initialize(false);
    }

    public final void initialize(boolean isReinit) {
        update(createInitialContext(isReinit));
    }

    protected FoldingStructureComputationContext createInitialContext(boolean isReinit) {
        initializePreferences(fStore);
        fInput = getInputElement();
        if (fInput == null)
            return null;

        // don't auto collapse if reinitializing
        return createContext((isReinit) ? false : true);
    }

    protected FoldingStructureComputationContext createInitialContext() {
        return createInitialContext(true);
    }

    protected 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 IModelElement getInputElement() {
        if (fEditor == null)
            return null;
        // TODO: multiple languages support
        return ((ScriptEditor) fEditor).getInputModelElement();
    }

    private void update(FoldingStructureComputationContext ctx) {
        if (ctx == null)
            return;
        Map<Annotation, Position> additions = new HashMap<Annotation, Position>();
        List<Annotation> deletions = new ArrayList<Annotation>();
        List<Annotation> updates = new ArrayList<Annotation>();
        if (!computeFoldingStructure(ctx)) {
            return;
        }
        Map<Annotation, Position> updated = ctx.fMap;
        Map<SourceRangeStamp, List<Tuple>> previous = computeCurrentStructure(ctx);
        for (Iterator<Annotation> e = updated.keySet().iterator(); e.hasNext();) {
            ScriptProjectionAnnotation newAnnotation = (ScriptProjectionAnnotation) e.next();
            SourceRangeStamp stamp = newAnnotation.getStamp();
            Position newPosition = updated.get(newAnnotation);
            List<Tuple> annotations = previous.get(stamp);
            if (annotations == null) {
                additions.put(newAnnotation, newPosition);
            } else {
                Iterator<Tuple> x = annotations.iterator();
                boolean matched = false;
                while (x.hasNext()) {
                    Tuple tuple = x.next();
                    ScriptProjectionAnnotation existingAnnotation = tuple.annotation;
                    Position existingPosition = tuple.position;
                    if (newAnnotation.isComment() == existingAnnotation.isComment()) {
                        if (existingPosition != null
                                && (!newPosition.equals(existingPosition) || ctx.allowCollapsing()
                                        && existingAnnotation.isCollapsed() != newAnnotation.isCollapsed())) {
                            existingPosition.setOffset(newPosition.getOffset());
                            existingPosition.setLength(newPosition.getLength());
                            if (ctx.allowCollapsing()
                                    && existingAnnotation.isCollapsed() != newAnnotation.isCollapsed())
                                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())
                    previous.remove(stamp);
            }
        }
        for (List<Tuple> list : previous.values()) {
            int size = list.size();
            for (int i = 0; i < size; i++)
                deletions.add(list.get(i).annotation);
        }
        Annotation[] removals = new Annotation[deletions.size()];
        deletions.toArray(removals);
        Annotation[] changes = new Annotation[updates.size()];
        updates.toArray(changes);
        ctx.getModel().modifyAnnotations(removals, additions, changes);
    }

    private boolean computeFoldingStructure(FoldingStructureComputationContext ctx) {
        try {
            String contents = ((ISourceReference) fInput).getSource();
            return computeFoldingStructure(contents, ctx);
        } catch (ModelException e) {
            return false;
        }
    }

    protected boolean computeFoldingStructure(String contents, FoldingStructureComputationContext ctx) {
        CodeBlock[] blockRegions = getCodeBlocks(contents);
        if (blockRegions == null) {
            return false;
        }

        if (fCommentsFolding) {
            IRegion[] commentRegions = computeCommentsRanges(contents);
            addDocAnnotations(contents, ctx, commentRegions, false);
        }

        String docPartition = getDocPartition();
        if (fDocsFolding && docPartition != null) {
            IRegion[] commentRegions = computeCommentsRanges(contents, docPartition);
            addDocAnnotations(contents, ctx, commentRegions, true);
        }

        // 2. Compute blocks regions
        final Document d = new Document(contents);
        final MethodCollector methodCollector = new MethodCollector();
        if (fInput != null) {
            try {
                fInput.accept(methodCollector);
            } catch (ModelException e) {
                // empty
            }
        }
        for (int i = 0; i < blockRegions.length; i++) {
            CodeBlock codeBlock = blockRegions[i];

            if (!mayCollapse(codeBlock.statement, ctx))
                continue;

            boolean collapseCode = initiallyCollapse(codeBlock.statement, ctx);
            IRegion reg = codeBlock.region;

            // code
            boolean multiline = false;
            try {
                multiline = isMultilineRegion(d, reg);
            } catch (BadLocationException e) {
                // nothing to do
            }
            IRegion normalized = alignRegion(reg, ctx);
            if (normalized != null && multiline) {
                Position position = createMemberPosition(normalized);
                if (position != null) {
                    try {
                        int len = normalized.getOffset() + normalized.getLength();
                        if (contents.length() == len + 1) {
                            len = len - 1;
                        }
                        if (contents.length() >= len) {
                            int hash = contents.substring(normalized.getOffset(), len).hashCode();
                            IModelElement element = null;

                            if (codeBlock.statement instanceof MethodDeclaration) {
                                MethodDeclaration meth = (MethodDeclaration) codeBlock.statement;
                                hash = meth.getName().hashCode();
                                element = methodCollector.get(meth.getNameStart(),
                                        meth.getNameEnd() - meth.getNameStart());
                            }
                            SourceRangeStamp codeStamp = new SourceRangeStamp(hash, normalized.getLength());
                            ScriptProjectionAnnotation annotation = new ScriptProjectionAnnotation(collapseCode,
                                    false, codeStamp, element);
                            ctx.addProjectionRange(annotation, position);
                        }
                    } catch (StringIndexOutOfBoundsException e) {
                        if (DLTKCore.DEBUG) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return true;
    }

    private void addDocAnnotations(String contents, FoldingStructureComputationContext ctx,
            IRegion[] commentRegions, boolean isDoc) {
        if (commentRegions.length == 0) {
            return;
        }
        final IElementCommentResolver commentResolver = fInput != null
                ? createElementCommentResolver(fInput, contents)
                : null;
        for (int i = 0; i < commentRegions.length; i++) {
            IRegion normalized = alignRegion(commentRegions[i], ctx);
            if (normalized == null) {
                continue;
            }

            Position position = createCommentPosition(normalized);
            if (position == null) {
                continue;
            }

            int hash = contents.substring(normalized.getOffset(), normalized.getOffset() + normalized.getLength())
                    .hashCode();
            final IModelElement element;
            if (commentResolver != null) {
                element = commentResolver.getElementByCommentPosition(position.offset, 0);
            } else {
                element = null;
            }

            boolean initCollapse = (isDoc) ? initiallyCollapseDocs(normalized, ctx)
                    : initiallyCollapseComments(normalized, ctx);

            ctx.addProjectionRange(new ScriptProjectionAnnotation(initCollapse, true,
                    new SourceRangeStamp(hash, normalized.getLength()), element), position);
        }
    }

    /**
     * @param modelElement
     * @param contents
     * @return
     */
    public IElementCommentResolver createElementCommentResolver(IModelElement modelElement, String contents) {
        return new DefaultElementCommentResolver((ISourceModule) modelElement, contents);
    }

    protected static class CodeBlock {
        public ASTNode statement;
        public IRegion region;

        /**
         * Represents foldable statement.
         * 
         * @param s
         *            AST statement
         * @param r
         *            <b>Absolute</b> statement position in source file
         */
        public CodeBlock(ASTNode s, IRegion r) {
            this.statement = s;
            this.region = r;
        }
    }

    protected int getMinimalFoldableLinesCount() {
        return fBlockLinesMin;
    }

    protected void initializePreferences(IPreferenceStore store) {
        fBlockLinesMin = store.getInt(PreferenceConstants.EDITOR_FOLDING_LINES_LIMIT);

        fDocsFolding = store.getBoolean(PreferenceConstants.EDITOR_DOCS_FOLDING_ENABLED);

        fCommentsFolding = store.getBoolean(PreferenceConstants.EDITOR_COMMENTS_FOLDING_ENABLED);

        fFoldNewLines = store.getBoolean(PreferenceConstants.EDITOR_COMMENT_FOLDING_JOIN_NEWLINES);

        fInitCollapseComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INIT_COMMENTS);

        fInitCollapseHeaderComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INIT_HEADER_COMMENTS);

        fInitCollapseDocs = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INIT_DOCS);

        fInitCollapseClasses = store.getBoolean(getInitiallyFoldClassesKey());
        fInitCollapseMethods = store.getBoolean(getInitiallyFoldMethodsKey());
    }

    /**
     * Returns the preference key used to indicate if classes should be
     * 'initially' folded.
     * 
     * <p>
     * Sub-classes may override this method to provide an alternative preference
     * key if they are not using the one in {@link PreferenceConstants}.
     * </p>
     */
    protected String getInitiallyFoldClassesKey() {
        return PreferenceConstants.EDITOR_FOLDING_INIT_CLASSES;
    }

    /**
     * Returns the preference key used to indicate if methods should be
     * 'initially' folded.
     * 
     * <p>
     * Sub-classes may override this method to provide an alternative preference
     * key if they are not using the one in {@link PreferenceConstants}.
     * </p>
     */
    protected String getInitiallyFoldMethodsKey() {
        return PreferenceConstants.EDITOR_FOLDING_INIT_METHODS;
    }

    protected boolean isEmptyRegion(IDocument d, ITypedRegion r) throws BadLocationException {
        return isEmptyRegion(d, r.getOffset(), r.getLength());
    }

    /**
     * Tests if the specified region contains only space or tab characters.
     * 
     * @param document
     * @param region
     * @return
     * @throws BadLocationException
     * @since 2.0
     */
    protected boolean isBlankRegion(IDocument document, ITypedRegion region) throws BadLocationException {
        String value = document.get(region.getOffset(), region.getLength());
        for (int i = 0; i < value.length(); ++i) {
            char ch = value.charAt(i);
            if (ch != ' ' && ch != '\t') {
                return false;
            }
        }
        return true;
    }

    protected boolean isEmptyRegion(IDocument d, int offset, int length) throws BadLocationException {
        return d.get(offset, length).trim().length() == 0;
    }

    protected boolean isMultilineRegion(IDocument d, IRegion region) throws BadLocationException {
        int line1 = d.getLineOfOffset(region.getOffset());
        int line2 = d.getLineOfOffset(region.getOffset() + region.getLength());
        final int foldMinLines = getMinimalFoldableLinesCount();
        if (foldMinLines > 0) {
            return line2 - line1 + 1 >= foldMinLines;
        } else {
            return line1 != line2;
        }
    }

    /**
     * Creates a comment folding position from an
     * {@link #alignRegion(IRegion, AbstractASTFoldingStructureProvider.FoldingStructureComputationContext)
     * aligned} region.
     * 
     * @param aligned
     *            an aligned region
     * @return a folding position corresponding to <code>aligned</code>
     */
    protected final Position createCommentPosition(IRegion aligned) {
        return new CommentPosition(aligned.getOffset(), aligned.getLength());
    }

    /**
     * Creates a folding position that remembers its member from an
     * {@link #alignRegion(IRegion, AbstractASTFoldingStructureProvider.FoldingStructureComputationContext)
     * aligned} region.
     * 
     * @param aligned
     *            an aligned region
     * 
     * @return a folding position corresponding to <code>aligned</code>
     */
    protected final Position createMemberPosition(IRegion aligned) {
        return new ScriptElementPosition(aligned.getOffset(), aligned.getLength());
    }

    /**
     * Aligns <code>region</code> to start and end at a line offset. The
     * region's start is decreased to the next line offset, and the end offset
     * increased to the next line start or the end of the document.
     * <code>null</code> is returned if <code>region</code> is <code>null</code>
     * itself or does not comprise at least one line delimiter, as a single line
     * cannot be folded.
     * 
     * @param region
     *            the region to align, may be <code>null</code>
     * @param ctx
     *            the folding context
     * @return a region equal or greater than <code>region</code> that is
     *         aligned with line offsets, <code>null</code> if the region is too
     *         small to be foldable (e.g. covers only one line)
     */
    protected 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(region.getOffset() + region.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 ProjectionAnnotationModel getModel() {
        return (ProjectionAnnotationModel) fEditor.getAdapter(ProjectionAnnotationModel.class);
    }

    private IDocument getDocument() {
        IDocumentProvider provider = fEditor.getDocumentProvider();
        return provider.getDocument(fEditor.getEditorInput());
    }

    private Map<SourceRangeStamp, List<Tuple>> computeCurrentStructure(FoldingStructureComputationContext ctx) {
        Map<SourceRangeStamp, List<Tuple>> map = new HashMap<SourceRangeStamp, List<Tuple>>();
        ProjectionAnnotationModel model = ctx.getModel();
        Iterator<?> e = model.getAnnotationIterator();
        while (e.hasNext()) {
            Object annotation = e.next();
            if (annotation instanceof ScriptProjectionAnnotation) {
                ScriptProjectionAnnotation ann = (ScriptProjectionAnnotation) annotation;
                Position position = model.getPosition(ann);
                List<Tuple> list = map.get(ann.getStamp());
                if (list == null) {
                    list = new ArrayList<Tuple>(2);
                    map.put(ann.getStamp(), list);
                }
                list.add(new Tuple(ann, position));
            }
        }
        Comparator<Tuple> comparator = new Comparator<Tuple>() {
            public int compare(Tuple o1, Tuple o2) {
                return o1.position.getOffset() - o2.position.getOffset();
            }
        };
        for (Iterator<List<Tuple>> it = map.values().iterator(); it.hasNext();) {
            List<Tuple> list = it.next();
            Collections.sort(list, comparator);
        }
        return map;
    }

    /*
     * @see IScriptFoldingStructureProviderExtension#collapseMembers()
     */
    public final void collapseMembers() {
        modifyFiltered(fMemberFilter, false);
    }

    /*
     * @see IScriptFoldingStructureProviderExtension#collapseComments()
     */
    public final void collapseComments() {
        modifyFiltered(fCommentFilter, false);
    }

    /**
     * Collapses or expands 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
     */
    private void modifyFiltered(Filter filter, boolean expand) {
        if (!isInstalled())
            return;
        ProjectionAnnotationModel model = getModel();
        if (model == null)
            return;
        List<Annotation> modified = new ArrayList<Annotation>();
        Iterator<?> iter = model.getAnnotationIterator();
        while (iter.hasNext()) {
            Object annotation = iter.next();
            if (annotation instanceof ScriptProjectionAnnotation) {
                ScriptProjectionAnnotation annot = (ScriptProjectionAnnotation) annotation;
                if (expand == annot.isCollapsed() && filter.match(annot)) {
                    if (expand)
                        annot.markExpanded();
                    else
                        annot.markCollapsed();
                    modified.add(annot);
                }
            }
        }
        model.modifyAnnotations(null, null, modified.toArray(new Annotation[modified.size()]));
    }

    protected abstract String getPartition();

    protected abstract String getCommentPartition();

    protected String getDocPartition() {
        return null;
    }

    protected abstract IPartitionTokenScanner getPartitionScanner();

    protected abstract String getNatureId();

    protected abstract String[] getPartitionTypes();

    protected abstract ILog getLog();

    protected FoldingASTVisitor getFoldingVisitor(int offset) {
        return new FoldingASTVisitor(offset);
    }

    protected static class FoldingASTVisitor extends ASTVisitor {
        private final List<CodeBlock> result = new ArrayList<CodeBlock>();
        private final int offset;

        protected FoldingASTVisitor(int offset) {
            this.offset = offset;
        }

        @Override
        public boolean visit(MethodDeclaration s) throws Exception {
            add(s);
            return super.visit(s);
        }

        @Override
        public boolean visit(TypeDeclaration s) throws Exception {
            add(s);
            return super.visit(s);
        }

        public CodeBlock[] getResults() {
            return result.toArray(new CodeBlock[result.size()]);
        }

        protected final void add(ASTNode s) {
            int start = offset + s.sourceStart();
            int end = s.sourceEnd() - s.sourceStart();

            result.add(new CodeBlock(s, new Region(start, end)));
        }

        protected void add(CodeBlock block) {
            result.add(block);
        }

    }

    // TODO: multiple languages support
    protected ISourceParser getSourceParser() {
        final IProject project = fInput != null ? fInput.getScriptProject().getProject() : null;
        return DLTKLanguageManager.getSourceParser(project, getNatureId());
    }

    /**
     * Should locate all statements and return
     * 
     * @param code
     */
    protected CodeBlock[] getCodeBlocks(String code) {
        return getCodeBlocks(code, 0);
    }

    protected CodeBlock[] getCodeBlocks(String code, int offset) {
        ModuleDeclaration decl = parse(code, offset);
        if (decl == null) {
            return null;
        }
        return buildCodeBlocks(decl, offset);
    }

    protected final IModelElement getModuleElement() {
        return fInput;
    }

    protected final ModuleDeclaration parse(String code, int offset) {
        if (offset == 0 && fInput instanceof ISourceModule) {
            final ISourceModule module = (ISourceModule) fInput;
            try {
                // TODO: DLTK multiples languages
                if (code.equals(module.getSource()) && module.getElementName().endsWith(".g")) {
                    // use the cache luke! ;)
                    return SourceParserUtil.getModuleDeclaration(module);
                }
            } catch (ModelException e) {
                getLog().log(new Status(IStatus.WARNING, DLTKUIPlugin.PLUGIN_ID, e.getMessage(), e));
            }
        }
        ISourceModule sm = (ISourceModule) getInputElement();
        IPath path = sm.getPath();
        return (ModuleDeclaration) getSourceParser().parse(new ModuleSource(path.toString(), code), null);
    }

    protected CodeBlock[] buildCodeBlocks(ModuleDeclaration decl, int offset) {
        FoldingASTVisitor visitor = getFoldingVisitor(offset);

        try {
            decl.traverse(visitor);
        } catch (Exception e) {
            if (DLTKCore.DEBUG) {
                e.printStackTrace();
            }
        }

        return visitor.getResults();
    }

    /**
     * Returns is it possible to collapse statement, or it should never be
     * folded
     * 
     * @param s
     * @param ctx
     */
    protected boolean mayCollapse(ASTNode s, FoldingStructureComputationContext ctx) {
        if (s instanceof TypeDeclaration) {
            return true;
        }

        if (s instanceof MethodDeclaration) {
            return true;
        }

        return false;
    }

    protected boolean initiallyCollapse(ASTNode s, FoldingStructureComputationContext ctx) {
        if (ctx.allowCollapsing()) {
            return initiallyCollapse(s);
        }

        return false;
    }

    protected boolean initiallyCollapse(ASTNode s) {
        // classes, modules, etc
        if (s instanceof TypeDeclaration && fInitCollapseClasses) {
            return true;
        }

        // methods, subroutines, etc
        if (s instanceof MethodDeclaration && fInitCollapseMethods) {
            return true;
        }

        return false;
    }

    protected boolean initiallyCollapseComments(IRegion commentRegion, FoldingStructureComputationContext ctx) {
        if (ctx.allowCollapsing()) {
            return isHeaderRegion(commentRegion, ctx) ? fInitCollapseHeaderComments : fInitCollapseComments;
        }
        return false;
    }

    protected boolean initiallyCollapseDocs(IRegion commentRegion, FoldingStructureComputationContext ctx) {
        if (ctx.allowCollapsing()) {
            return fInitCollapseDocs;
        }

        return false;
    }

    /**
     * Checks if the specified region is located at the beginning of the
     * document
     * 
     * @param region
     * @param ctx
     */
    protected boolean isHeaderRegion(IRegion region, FoldingStructureComputationContext ctx) {
        final int offset = region.getOffset();
        if (offset == 0) {
            return true;
        } else if (offset < 100) {
            try {
                return isEmptyRegion(ctx.getDocument(), 0, offset);
            } catch (BadLocationException e) {
                //
            }
        }
        return false;
    }

    /**
     * Installs a partitioner with <code>document</code>.
     * 
     * @param document
     *            the document
     */
    private void installDocumentStuff(Document document) {
        IDocumentPartitioner partitioner = getDocumentPartitioner();
        partitioner.connect(document);
        document.setDocumentPartitioner(getPartition(), partitioner);
    }

    protected IDocumentPartitioner getDocumentPartitioner() {
        return new FastPartitioner(getPartitionScanner(), getPartitionTypes());
    }

    /**
     * Removes partitioner with <code>document</code>.
     * 
     * @param document
     *            the document
     */
    private void removeDocumentStuff(Document document) {
        document.setDocumentPartitioner(getPartition(), null);
    }

    public void expandElements(final IModelElement[] array) {
        modifyFiltered(new Filter() {

            public boolean match(ScriptProjectionAnnotation annotation) {
                IModelElement element = annotation.getElement();
                if (element == null)
                    return false;
                for (int a = 0; a < array.length; a++) {
                    IModelElement e = array[a];
                    if (e.equals(element)) {
                        return true;
                    }
                }
                return false;
            }

        }, true);
    }

    public void collapseElements(IModelElement[] modelElements) {
        // empty implementation
    }

    private ITypedRegion getRegion(IDocument d, int offset) throws BadLocationException {
        return TextUtilities.getPartition(d, getPartition(), offset, true);
    }

    protected IRegion[] computeCommentsRanges(String contents) {
        // for backwards compatibility incase anyone has overridden this..
        return computeCommentsRanges(contents, getCommentPartition());
    }

    protected IRegion[] computeCommentsRanges(String contents, String partition) {
        try {
            if (contents == null)
                return new IRegion[0];
            Document d = new Document(contents);
            installDocumentStuff(d);
            List<ITypedRegion> docRegionList = new ArrayList<ITypedRegion>();
            int offset = 0;
            for (;;) {
                try {
                    ITypedRegion region = getRegion(d, offset);
                    docRegionList.add(region);
                    offset = region.getLength() + region.getOffset() + 1;
                } catch (BadLocationException e1) {
                    break;
                }
            }
            ITypedRegion start = null;
            ITypedRegion lastRegion = null;
            List<IRegion> regions = new ArrayList<IRegion>();
            for (ITypedRegion region : docRegionList) {
                if (region.getType().equals(partition) && startsAtLineBegin(d, region)) {
                    if (start == null)
                        start = region;
                } else if (start != null
                        && (isBlankRegion(d, region) || isEmptyRegion(d, region) && collapseEmptyLines())) {
                    // blanks or empty lines
                    // TODO introduce line limit for collapseEmptyLines() ?
                } else {
                    if (start != null) {
                        int offset0 = start.getOffset();
                        int length0 = lastRegion.getOffset() + lastRegion.getLength() - offset0 - 1;
                        length0 = contents.substring(offset0, offset0 + length0).trim().length();
                        IRegion fullRegion = new Region(offset0, length0);
                        if (isMultilineRegion(d, fullRegion)) {
                            regions.add(fullRegion);
                        }
                    }
                    start = null;
                }
                lastRegion = region;
            }
            if (start != null) {
                int offset0 = start.getOffset();
                int length0 = lastRegion.getOffset() - offset0 + lastRegion.getLength() - 1;
                IRegion fullRegion = new Region(offset0, length0);
                if (isMultilineRegion(d, fullRegion)) {
                    regions.add(fullRegion);
                }
            }
            prepareRegions(d, regions);
            removeDocumentStuff(d);
            return regions.toArray(new IRegion[regions.size()]);
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
        return new IRegion[0];
    }

    /**
     * @param d
     * @param regions
     * @since 2.0
     */
    protected void prepareRegions(Document d, List<IRegion> regions) {
        // override in descendants
    }

    private boolean startsAtLineBegin(Document d, ITypedRegion region) throws BadLocationException {
        int lineStart = d.getLineOffset(d.getLineOfOffset(region.getOffset()));
        if (lineStart != region.getOffset()) {
            if (!isEmptyRegion(d, lineStart, region.getOffset() - lineStart)) {
                return false;
            }
        }
        return true;
    }

    protected boolean collapseEmptyLines() {
        return fFoldNewLines;
    }

    /**
     * @deprecated
     */
    protected final void getElementCommentResolver() {
        // will be deleted
    }

    public static class MethodCollector implements IModelElementVisitor {
        private final Map<SourceRange, IModelElement> methodByNameRange = new HashMap<SourceRange, IModelElement>();

        public boolean visit(IModelElement element) {

            if (element instanceof SourceMethod) {
                try {
                    final ISourceRange nameRange = ((SourceMethod) element).getNameRange();
                    methodByNameRange.put(new SourceRange(nameRange), element);
                } catch (ModelException e) {
                    // empty
                }
            }
            return true;
        }

        /**
         * @param offset
         * @param length
         */
        public IModelElement get(int offset, int length) {
            return methodByNameRange.get(new SourceRange(offset, length));
        }

    }
}