com.google.dart.tools.ui.text.folding.DefaultDartFoldingStructureProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.google.dart.tools.ui.text.folding.DefaultDartFoldingStructureProvider.java

Source

/*
 * Copyright (c) 2012, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.dart.tools.ui.text.folding;

import com.google.dart.engine.error.AnalysisError;
import com.google.dart.engine.error.AnalysisErrorListener;
import com.google.dart.engine.scanner.StringScanner;
import com.google.dart.engine.scanner.Token;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.model.CompilationUnit;
import com.google.dart.tools.core.model.DartElement;
import com.google.dart.tools.core.model.DartElementDelta;
import com.google.dart.tools.core.model.DartModelException;
import com.google.dart.tools.core.model.DartModelStatus;
import com.google.dart.tools.core.model.ElementChangedEvent;
import com.google.dart.tools.core.model.ParentElement;
import com.google.dart.tools.core.model.SourceRange;
import com.google.dart.tools.core.model.SourceReference;
import com.google.dart.tools.core.model.Type;
import com.google.dart.tools.core.model.TypeMember;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.DartX;
import com.google.dart.tools.ui.PreferenceConstants;
import com.google.dart.tools.ui.internal.text.editor.DartEditor;
import com.google.dart.tools.ui.internal.text.editor.EditorUtility;
import com.google.dart.tools.ui.internal.text.functions.DocumentCharacterIterator;

import org.eclipse.core.runtime.Assert;
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.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Updates the projection model of a class file or compilation unit.
 * <p>
 * Clients may instantiate or subclass. Subclasses must make sure to always call the superclass'
 * code when overriding methods that are marked with "subclasses may extend".
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class DefaultDartFoldingStructureProvider
        implements IDartFoldingStructureProvider, IDartFoldingStructureProviderExtension {

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

        private DartElement dartElement;
        private boolean isComment;

        /**
         * 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 Dart element this annotation refers to
         * @param isComment <code>true</code> for a foldable comment, <code>false</code> for a foldable
         *          code element
         */
        public DartProjectionAnnotation(boolean isCollapsed, DartElement element, boolean isComment) {
            super(isCollapsed);
            this.dartElement = element;
            this.isComment = isComment;
        }

        @Override
        public String toString() {
            return "DartProjectionAnnotation:\n" + //$NON-NLS-1$
                    "\telement: \t" + dartElement.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$
        }

        DartElement getElement() {
            return dartElement;
        }

        boolean isComment() {
            return isComment;
        }

        void setElement(DartElement element) {
            dartElement = element;
        }

        void setIsComment(boolean isComment) {
            this.isComment = isComment;
        }
    }

    /**
     * A context that contains the information needed to compute the folding structure of a Dart
     * compilation unit. Computed folding regions are collected via
     * {@linkplain #addProjectionRange(DefaultDartFoldingStructureProvider.DartProjectionAnnotation, Position)
     * addProjectionRange}.
     */
    protected final class FoldingStructureComputationContext {
        private final ProjectionAnnotationModel model;
        private final IDocument document;

        private final boolean allowCollapsing;

        private Type firstType;
        private boolean hasHeaderComment;
        private LinkedHashMap map = new LinkedHashMap();
        private TokenStream tokenStream;

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

        /**
         * 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(DartProjectionAnnotation annotation, Position position) {
            map.put(annotation, position);
        }

        /**
         * 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 allowCollapsing;
        }

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

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

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

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

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

        boolean hasFirstType() {
            return firstType != null;
        }

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

        private Type getFirstType() {
            return firstType;
        }

        private ProjectionAnnotationModel getModel() {
            return model;
        }

        private TokenStream getScanner() {
            return getScanner(0);
        }

        private TokenStream getScanner(int start) {
            tokenStream.begin(start);
            return tokenStream;
        }

        private boolean hasHeaderComment() {
            return hasHeaderComment;
        }

        private void setFirstType(Type type) {
            if (hasFirstType()) {
                throw new IllegalStateException();
            }
            firstType = type;
        }

        private void setHasHeaderComment() {
            hasHeaderComment = true;
        }

        private void setScannerSource(String source) throws DartModelException {
            this.tokenStream = new TokenStream(source);
        }
    }

    /**
     * Matches comments.
     */
    private static final class CommentFilter implements Filter {
        @Override
        public boolean match(DartProjectionAnnotation annotation) {
            if (annotation.isComment() && !annotation.isMarkedDeleted()) {
                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);
        }

        @Override
        public int computeCaptionOffset(IDocument document) {
            DocumentCharacterIterator sequence = new DocumentCharacterIterator(document, offset, offset + length);
            return findFirstContent(sequence, 0);
        }

        @Override
        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 findPrefixEnd(final CharSequence content) {
        //         // return the index after the leading '/*' or '/**'
        //         int len= content.length();
        //         int i= 0;
        //         while (i < len && isWhiteSpace(content.charAt(i)))
        //            i++;
        //         if (len >= i + 2 && content.charAt(i) == '/' && content.charAt(i + 1) == '*')
        //            if (len >= i + 3 && content.charAt(i + 2) == '*')
        //               return i + 3;
        //            else
        //               return i + 2;
        //         else
        //            return i;
        //      }
        //
        //      private boolean isWhiteSpace(char c) {
        //         return c == ' ' || c == '\t';
        //      }

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

    /**
     * Projection position that will return two foldable regions: one folding away the lines before
     * the one containing the simple name of the JavaScript element, one folding away any lines after
     * the caption.
     */
    private static final class DartElementPosition extends Position implements IProjectionPosition {

        private TypeMember fMember;

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

        @Override
        public int computeCaptionOffset(IDocument document) throws BadLocationException {
            int nameStart = offset;
            try {
                // need a reconcile here?
                SourceRange nameRange = fMember.getNameRange();
                if (nameRange != null) {
                    nameStart = nameRange.getOffset();
                }
            } catch (DartModelException e) {
                // ignore and use default
            }

            return nameStart - offset;
        }

        @Override
        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.
                 */
                SourceRange nameRange = fMember.getNameRange();
                if (nameRange != null) {
                    nameStart = nameRange.getOffset();
                }

            } catch (DartModelException 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;
        }

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

    }

    /**
     * Matches Dart elements contained in a certain set.
     */
    private static final class DartElementSetFilter implements Filter {
        private final Set<? extends DartElement> fSet;
        private final boolean fMatchCollapsed;

        private DartElementSetFilter(Set<? extends DartElement> set, boolean matchCollapsed) {
            fSet = set;
            fMatchCollapsed = matchCollapsed;
        }

        @Override
        public boolean match(DartProjectionAnnotation annotation) {
            boolean stateMatch = fMatchCollapsed == annotation.isCollapsed();
            if (stateMatch && !annotation.isComment() && !annotation.isMarkedDeleted()) {
                DartElement element = annotation.getElement();
                if (fSet.contains(element)) {
                    return true;
                }
            }
            return false;
        }
    }

    private class ElementChangedListener implements com.google.dart.tools.core.model.ElementChangedListener {

        @Override
        public void elementChanged(ElementChangedEvent e) {
            DartElementDelta delta = findElement(fInput, e.getDelta());
            if (delta != null
                    && (delta.getFlags() & (DartElementDelta.F_CONTENT | DartElementDelta.F_CHILDREN)) != 0) {

                DartX.todo();
                //        if (shouldIgnoreDelta(e.getDelta().getCompilationUnitAST(), delta))
                //          return;

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

        private DartElementDelta findElement(DartElement target, DartElementDelta delta) {

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

            DartElement element = delta.getElement();

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

            DartElementDelta[] children = delta.getAffectedChildren();

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

            return null;
        }

        /**
         * 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 JavaScript element delta for the given AST element
         * @return <code>true</code> if the delta should be ignored
         */
        @SuppressWarnings("unused")
        private boolean shouldIgnoreDelta(CompilationUnit ast, DartElementDelta delta) {
            if (ast == null) {
                return false; // can't compute
            }

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

            DartEditor editor = dartEditor;
            if (editor == null || editor.getCachedSelectedRange() == null) {
                return false; // can't compute
            }

            //      try {
            //        if (delta.getAffectedChildren().length == 1
            //            && delta.getAffectedChildren()[0].getElement() instanceof DartImportContainer) {
            //          DartElement elem = SelectionConverter.getElementAtOffset(
            //              ast.getJavaElement(),
            //              new TextSelection(editor.getCachedSelectedRange().x,
            //                  editor.getCachedSelectedRange().y));
            //          if (!(elem instanceof IImportDeclaration))
            //            return false;
            //
            //        }
            //      } catch (DartModelException e) {
            //        return false; // can't compute
            //      }

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

            DartX.todo();
            //      if (caretLine > 0 && ast != null) {
            //        Problem[] problems = ast.getProblems();
            //        for (int i = 0; i < problems.length; i++) {
            //          if (problems[i].isError()
            //              && caretLine == problems[i].getSourceLineNumber())
            //            return true;
            //        }
            //      }

            return false;
        }
    }

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

    /**
     * Matches members.
     */
    private static final class MemberFilter implements Filter {
        @Override
        public boolean match(DartProjectionAnnotation annotation) {
            if (!annotation.isComment() && !annotation.isMarkedDeleted()) {
                DartElement element = annotation.getElement();
                if (element instanceof TypeMember) {
                    if (element.getElementType() != DartElement.TYPE
                            || ((TypeMember) element).getDeclaringType() != null) {
                        return true;
                    }
                }
            }
            return false;
        }
    }

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

        @Override
        public void projectionDisabled() {
            handleProjectionDisabled();
        }

        @Override
        public void projectionEnabled() {
            handleProjectionEnabled();
        }
    }

    private static class TokenStream {
        StringScanner scanner;
        Token firstToken;
        Token currentToken;
        int begin;

        TokenStream(String source) throws DartModelException {
            final boolean[] errorFound = { false };
            AnalysisErrorListener listener = new AnalysisErrorListener() {
                @Override
                public void onError(AnalysisError error) {
                    errorFound[0] = true;
                }
            };
            scanner = new StringScanner(null, source, listener);
            if (errorFound[0]) {
                throw new DartModelException((DartModelStatus) null);
            } else {
                firstToken = scanner.tokenize();
                currentToken = firstToken;
                begin = 0;
            }
        }

        void begin(int start) {
            if (start == begin) {
                return;
            }
            if (start < begin) {
                begin = 0;
                currentToken = firstToken;
            }
            while (begin < start) {
                currentToken = currentToken.getNext();
                begin = currentToken.getOffset();
            }
        }

        Token next() {
            Token next = currentToken;
            currentToken = currentToken.getNext();
            return next;
        }
    }

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

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

    static boolean isAvailable(SourceRange range) {
        return range != null && range.getOffset() != -1;
    }

    /* context and listeners */
    private DartEditor dartEditor;
    private ProjectionListener projectionListener;
    private DartElement fInput;

    private ElementChangedListener fElementListener;
    /* preferences */
    private boolean collapseDartDoc = false;
    private boolean collapseImportContainer = true;
    private boolean collapseInnerTypes = true;
    private boolean collapseMembers = false;

    private boolean collapseHeaderComments = true;
    /* 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 volatile int fUpdatingCount = 0;

    /**
     * Creates a new folding provider. It must be {@link #install(ITextEditor, ProjectionViewer)
     * 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 DefaultDartFoldingStructureProvider() {
    }

    @Override
    public final void collapseComments() {
        modifyFiltered(fCommentFilter, false);
    }

    @Override
    public final void collapseElements(DartElement[] elements) {
        Set set = new HashSet(Arrays.asList(elements));
        modifyFiltered(new DartElementSetFilter(set, false), false);
    }

    @Override
    public final void collapseMembers() {
        modifyFiltered(fMemberFilter, false);
    }

    @Override
    public final void expandElements(DartElement[] elements) {
        Set set = new HashSet(Arrays.asList(elements));
        modifyFiltered(new DartElementSetFilter(set, true), true);
    }

    @Override
    public final void initialize() {
        //    fUpdatingCount++;
        try {
            update(createInitialContext());
        } finally {
            //      fUpdatingCount--;
        }
    }

    @Override
    public void install(ITextEditor editor, ProjectionViewer viewer) {
        Assert.isLegal(editor != null);
        Assert.isLegal(viewer != null);

        internalUninstall();

        if (editor instanceof DartEditor) {
            projectionListener = new ProjectionListener(viewer);
            dartEditor = (DartEditor) editor;
        }
    }

    @Override
    public void uninstall() {
        internalUninstall();
    }

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

    /**
     * Computes the folding structure for a given {@link DartElement Dart element}. Computed
     * projection annotations are
     * {@link DefaultDartFoldingStructureProvider.FoldingStructureComputationContext#addProjectionRange(DefaultDartFoldingStructureProvider.DartProjectionAnnotation, Position)
     * added} to the computation context.
     * <p>
     * Subclasses may extend or replace. This implementation creates projection annotations for the
     * following elements:
     * <ul>
     * <li>top-level functions, fields, and typedefs
     * <li>members of types (not for top-level types)</li>
     * </ul>
     * </p>
     * 
     * @param element the Dart element to compute the folding structure for
     * @param ctx the computation context
     */
    protected void computeFoldingStructure(DartElement element, FoldingStructureComputationContext ctx) {

        boolean collapse = false;
        boolean collapseCode = true;
        switch (element.getElementType()) {

        case DartElement.IMPORT_CONTAINER:
            collapse = ctx.collapseImportContainer();
            break;
        case DartElement.TYPE:
            collapseCode = isInnerType((Type) element);
            collapse = ctx.collapseInnerTypes() && collapseCode;
            break;
        case DartElement.METHOD:
        case DartElement.FIELD:
        case DartElement.FUNCTION:
        case DartElement.FUNCTION_TYPE_ALIAS:
            collapse = ctx.collapseMembers();
            break;
        default:
            return;
        }

        IRegion[] regions = computeProjectionRanges((SourceReference) element, ctx);
        if (regions.length > 0) {
            // comments
            for (int i = 0; i < regions.length - 1; i++) {
                IRegion normalized = alignRegion(regions[i], ctx);
                if (normalized != null) {
                    Position position = createCommentPosition(normalized);
                    if (position != null) {
                        boolean commentCollapse;
                        if (i == 0 && (regions.length > 2 || ctx.hasHeaderComment())
                                && element == ctx.getFirstType()) {
                            commentCollapse = ctx.collapseHeaderComments();
                        } else {
                            commentCollapse = ctx.collapseDartDoc();
                        }
                        ctx.addProjectionRange(new DartProjectionAnnotation(commentCollapse, element, true),
                                position);
                    }
                }
            }
            // code
            if (collapseCode) {
                IRegion normalized = alignRegion(regions[regions.length - 1], ctx);
                if (normalized != null) {
                    Position position = element instanceof TypeMember
                            ? createMemberPosition(normalized, (TypeMember) element)
                            : createCommentPosition(normalized);
                    if (position != null) {
                        ctx.addProjectionRange(new DartProjectionAnnotation(collapse, element, false), position);
                    }
                }
            }
        }
    }

    /**
     * Computes the projection ranges for a given <code>SourceReference</code>. More than one range or
     * none at all may be returned. If there are no foldable regions, an empty array is returned.
     * <p>
     * The last region in the returned array (if not empty) describes the region for the java element
     * that implements the source reference. Any preceding regions describe Dart doc comments of that
     * element.
     * </p>
     * 
     * @param reference a Dart element that is a source reference
     * @param ctx the folding context
     * @return the regions to be folded
     */
    protected final IRegion[] computeProjectionRanges(SourceReference reference,
            FoldingStructureComputationContext ctx) {
        try {
            SourceRange range = reference.getSourceRange();
            if (!isAvailable(range)) {
                return new IRegion[0];
            }

            String contents = reference.getSource();
            if (contents == null) {
                return new IRegion[0];
            }

            List regions = new ArrayList();
            if (!ctx.hasFirstType() && reference instanceof Type) {
                ctx.setFirstType((Type) reference);
                IRegion headerComment = computeHeaderComment(ctx);
                if (headerComment != null) {
                    regions.add(headerComment);
                    ctx.setHasHeaderComment();
                }
            }

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

            int start = shift;
            Token token = scanner.next();
            start = token.getOffset();
            Token comment = token.getPrecedingComments();
            while (comment != null) {
                int s = token.getOffset();
                int l = token.getLength();
                regions.add(new Region(s, l));
                comment = comment.getNext();
                if (comment == token) {
                    comment = null;
                }
            }

            regions.add(new Region(start, shift + range.getLength() - start - 1));

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

        return new IRegion[0];
    }

    /**
     * Creates a comment folding position from an
     * {@link #alignRegion(IRegion, DefaultDartFoldingStructureProvider.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, DefaultDartFoldingStructureProvider.FoldingStructureComputationContext)
     * aligned} region.
     * 
     * @param aligned an aligned region
     * @param member the member to remember
     * @return a folding position corresponding to <code>aligned</code>
     */
    protected final Position createMemberPosition(IRegion aligned, TypeMember member) {
        return new DartElementPosition(aligned.getOffset(), aligned.getLength(), member);
    }

    /**
     * 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) {
            DartCore.removeElementChangedListener(fElementListener);
            fElementListener = 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() {
        // projectionEnabled messages are not always paired with projectionDisabled
        // i.e. multiple enabled messages may be sent out.
        // we have to make sure that we disable first when getting an enable
        // message.
        handleProjectionDisabled();

        if (isInstalled()) {
            initialize();
            fElementListener = new ElementChangedListener();
            try {
                DartCore.addElementChangedListener(fElementListener);
            } catch (Exception ex) {
                DartToolsPlugin.log(ex);
            }
        }
    }

    /**
     * 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 dartEditor != null;
    }

    private Map computeCurrentStructure(FoldingStructureComputationContext ctx) {
        Map map = new HashMap();
        ProjectionAnnotationModel model = ctx.getModel();
        Iterator e = model.getAnnotationIterator();
        while (e.hasNext()) {
            Object annotation = e.next();
            if (annotation instanceof DartProjectionAnnotation) {
                DartProjectionAnnotation java = (DartProjectionAnnotation) annotation;
                Position position = model.getPosition(java);
                Assert.isNotNull(position);
                List list = (List) map.get(java.getElement());
                if (list == null) {
                    list = new ArrayList(2);
                    map.put(java.getElement(), list);
                }
                list.add(new Tuple(java, position));
            }
        }

        Comparator comparator = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((Tuple) o1).position.getOffset() - ((Tuple) o2).position.getOffset();
            }
        };
        for (Iterator it = map.values().iterator(); it.hasNext();) {
            List list = (List) it.next();
            Collections.sort(list, comparator);
        }
        return map;
    }

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

            computeFoldingStructure(element, ctx);

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

    private void computeFoldingStructure(FoldingStructureComputationContext ctx) {
        ParentElement parent = (ParentElement) fInput;
        try {
            if (!(fInput instanceof SourceReference)) {
                return;
            }
            String source = ((SourceReference) fInput).getSource();
            if (source == null) {
                return;
            }

            ctx.setScannerSource(source);
            computeFoldingStructure(parent.getChildren(), ctx);
        } catch (DartModelException x) {
        }
    }

    private IRegion computeHeaderComment(FoldingStructureComputationContext ctx) throws DartModelException {
        // search at most up to the first type
        SourceRange range = ctx.getFirstType().getSourceRange();
        if (range == null) {
            return null;
        }

        TokenStream scanner = ctx.getScanner();

        int headerStart = -1;
        int headerEnd = -1;
        boolean foundComment = false;
        Token terminal = scanner.next();
        Token comment = terminal.getPrecedingComments();
        while (comment != null) {
            if (!foundComment) {
                headerStart = comment.getOffset();
            }
            headerEnd = comment.getEnd();
            foundComment = true;
            comment = comment.getNext();
            if (comment == terminal) {
                comment = null;
            }
        }

        if (headerEnd != -1) {
            return new Region(headerStart, headerEnd - headerStart);
        }
        return null;
    }

    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 FoldingStructureComputationContext createInitialContext() {
        initializePreferences();
        fInput = getInputElement();
        if (fInput == null) {
            return null;
        }

        return createContext(true);
    }

    /**
     * Finds a match for <code>tuple</code> in a collection of annotations. The positions for the
     * <code>DartProjectionAnnotation</code> instances in <code>annotations</code> can be found in the
     * passed <code>positionMap</code> or <code>cachedModel</code> if <code>positionMap</code> is
     * <code>null</code>.
     * <p>
     * A tuple is said to match another if their annotations have the same comment flag and their
     * position offsets are equal.
     * </p>
     * <p>
     * If a match is found, the annotation gets removed from <code>annotations</code>.
     * </p>
     * 
     * @param tuple the tuple for which we want to find a match
     * @param annotations collection of <code>JavaProjectionAnnotation</code>
     * @param positionMap a <code>Map&lt;Annotation, Position&gt;</code> or <code>null</code>
     * @param ctx the context
     * @return a matching tuple or <code>null</code> for no match
     */
    private Tuple findMatch(Tuple tuple, Collection annotations, Map positionMap,
            FoldingStructureComputationContext ctx) {
        Iterator it = annotations.iterator();
        while (it.hasNext()) {
            DartProjectionAnnotation annotation = (DartProjectionAnnotation) it.next();
            if (tuple.annotation.isComment() == annotation.isComment()) {
                Position position = positionMap == null ? ctx.getModel().getPosition(annotation)
                        : (Position) positionMap.get(annotation);
                if (position == null) {
                    continue;
                }

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

        return null;
    }

    private IDocument getDocument() {
        DartEditor editor = dartEditor;
        if (editor == null) {
            return null;
        }

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

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

    private DartElement getInputElement() {
        if (dartEditor == null) {
            return null;
        }
        return EditorUtility.getEditorInputDartElement(dartEditor, false);
    }

    private ProjectionAnnotationModel getModel() {
        return (ProjectionAnnotationModel) dartEditor.getAdapter(ProjectionAnnotationModel.class);
    }

    private void initializePreferences() {
        IPreferenceStore store = DartToolsPlugin.getDefault().getPreferenceStore();
        collapseInnerTypes = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_INNERTYPES);
        collapseImportContainer = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_IMPORTS);
        collapseDartDoc = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_JAVADOC);
        collapseMembers = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_METHODS);
        collapseHeaderComments = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_HEADERS);
    }

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

    /**
     * Returns <code>true</code> if <code>type</code> is not a top-level type, <code>false</code> if
     * it is.
     * 
     * @param type the type to test
     * @return <code>true</code> if <code>type</code> is an inner type
     */
    private boolean isInnerType(Type type) {
        return false;
    }

    /**
     * Matches deleted annotations to changed or added ones. A deleted annotation/position tuple that
     * has a matching addition / change is updated and marked as changed. The matching tuple is not
     * added (for additions) or marked as deletion instead (for changes). The result is that more
     * annotations are changed and fewer get deleted/re-added.
     * 
     * @param deletions list with deleted annotations
     * @param additions map with position to annotation mappings
     * @param changes list with changed annotations
     * @param ctx the context
     */
    private void match(List deletions, Map additions, List changes, FoldingStructureComputationContext ctx) {
        if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty())) {
            return;
        }

        List newDeletions = new ArrayList();
        List newChanges = new ArrayList();

        Iterator deletionIterator = deletions.iterator();
        while (deletionIterator.hasNext()) {
            DartProjectionAnnotation deleted = (DartProjectionAnnotation) 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) {
                DartElement element = match.annotation.getElement();
                deleted.setElement(element);
                deletedPosition.setLength(match.position.getLength());
                if (deletedPosition instanceof DartElementPosition && element instanceof TypeMember) {
                    DartElementPosition jep = (DartElementPosition) deletedPosition;
                    jep.setMember((TypeMember) element);
                }

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

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

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

    /**
     * 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 modified = new ArrayList();
        Iterator iter = model.getAnnotationIterator();
        while (iter.hasNext()) {
            Object annotation = iter.next();
            if (annotation instanceof DartProjectionAnnotation) {
                DartProjectionAnnotation java = (DartProjectionAnnotation) annotation;

                if (expand == java.isCollapsed() && filter.match(java)) {
                    if (expand) {
                        java.markExpanded();
                    } else {
                        java.markCollapsed();
                    }
                    modified.add(java);
                }

            }
        }

        model.modifyAnnotations(null, null, (Annotation[]) modified.toArray(new Annotation[modified.size()]));
    }

    private void update(FoldingStructureComputationContext ctx) {
        if (ctx == null) {
            return;
        }
        Map additions = new HashMap();
        List deletions = new ArrayList();
        List updates = new ArrayList();

        computeFoldingStructure(ctx);
        Map newStructure = ctx.map;
        Map oldStructure = computeCurrentStructure(ctx);

        Iterator e = newStructure.keySet().iterator();
        while (e.hasNext()) {
            DartProjectionAnnotation newAnnotation = (DartProjectionAnnotation) e.next();
            Position newPosition = (Position) newStructure.get(newAnnotation);

            DartElement 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() == DartElement.TYPE && isInnerType((Type) element);
            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();
                    DartProjectionAnnotation 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 = (Annotation[]) deletions.toArray(new Annotation[deletions.size()]);
        Annotation[] changedArray = (Annotation[]) updates.toArray(new Annotation[updates.size()]);
        ctx.getModel().modifyAnnotations(deletedArray, additions, changedArray);
        try {
            ctx.setScannerSource(""); // clear token stream
        } catch (DartModelException e1) {
        }
    }
}