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

Java tutorial

Introduction

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

Source

/*
 * Copyright (c) 2013, 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.ast.AstNode;
import com.google.dart.engine.ast.ClassMember;
import com.google.dart.engine.ast.CompilationUnit;
import com.google.dart.engine.ast.CompilationUnitMember;
import com.google.dart.engine.ast.Directive;
import com.google.dart.engine.ast.NamespaceDirective;
import com.google.dart.engine.ast.Statement;
import com.google.dart.engine.ast.visitor.BreadthFirstVisitor;
import com.google.dart.engine.ast.visitor.GeneralizingAstVisitor;
import com.google.dart.engine.element.ExecutableElement;
import com.google.dart.engine.error.BooleanErrorListener;
import com.google.dart.engine.scanner.CharSequenceReader;
import com.google.dart.engine.scanner.Scanner;
import com.google.dart.engine.scanner.Token;
import com.google.dart.engine.utilities.source.SourceRange;
import com.google.dart.tools.core.DartCoreDebug;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.DartUI;
import com.google.dart.tools.ui.PreferenceConstants;
import com.google.dart.tools.ui.internal.text.dart.IDartReconcilingListener;
import com.google.dart.tools.ui.internal.text.editor.CompilationUnitEditor;
import com.google.dart.tools.ui.internal.text.editor.DartEditor;
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.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;

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

/**
 * Updates the projection model of a compilation unit.
 */
public class DartFoldingStructureProvider
        implements IDartFoldingStructureProvider, IDartFoldingStructureProviderExtension {

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

        private AstNode node;
        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 node the Dart AST node 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, AstNode node, boolean isComment) {
            super(isCollapsed);
            this.node = node;
            this.isComment = isComment;
        }

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

        AstNode getElement() {
            return node;
        }

        boolean isComment() {
            return isComment;
        }

        void setElement(AstNode element) {
            node = 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(DartFoldingStructureProvider.DartProjectionAnnotation, Position)
     * addProjectionRange}.
     */
    protected class FoldingStructureComputationContext {

        private ProjectionAnnotationModel model;
        private IDocument document;
        private boolean allowCollapsing;
        private AstNode firstRef;
        private boolean hasHeaderComment;
        private Map<DartProjectionAnnotation, Position> map = new LinkedHashMap<DartProjectionAnnotation, Position>();
        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 classes should be collapsed.
         * 
         * @return <code>true</code> if classes should be collapsed
         */
        public boolean collapseClasses() {
            return allowCollapsing && collapseClasses;
        }

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

        /**
         * Returns <code>true</code> if top-level functions should be collapsed.
         * 
         * @return <code>true</code> if functions should be collapsed
         */
        public boolean collapseFunctions() {
            return allowCollapsing && collapseFunctions;
        }

        /**
         * 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 methods should be collapsed.
         * 
         * @return <code>true</code> if methods should be collapsed
         */
        public boolean collapseMembers() {
            return allowCollapsing && collapseMembers;
        }

        public boolean collapseStatements() {
            return false; // TODO(messick) Implement statement folding.
        }

        boolean hasFirstRef() {
            return firstRef != 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 AstNode getFirstRef() {
            return firstRef;
        }

        private ProjectionAnnotationModel getModel() {
            return model;
        }

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

        private boolean hasHeaderComment() {
            return hasHeaderComment;
        }

        private void setFirstRef(AstNode type) {
            if (hasFirstRef()) {
                throw new IllegalStateException();
            }
            firstRef = type;
        }

        private void setHasHeaderComment() {
            hasHeaderComment = true;
        }

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

    private static class CollapsibleNodeClassifier extends GeneralizingAstVisitor<CollapsibleNodeType> {

        @Override
        public CollapsibleNodeType visitClassMember(ClassMember node) {
            return CollapsibleNodeType.CLASS_MEMEBER;
        }

        @Override
        public CollapsibleNodeType visitCompilationUnitMember(CompilationUnitMember node) {
            return CollapsibleNodeType.TOP_LEVEL_DECL;
        }

        @Override
        public CollapsibleNodeType visitDirective(Directive node) {
            return CollapsibleNodeType.DIRECTIVE;
        }

        @Override
        public CollapsibleNodeType visitNamespaceDirective(NamespaceDirective node) {
            return CollapsibleNodeType.NAMESPACE_DIRECTIVE;
        }

        @Override
        public CollapsibleNodeType visitNode(AstNode node) {
            return CollapsibleNodeType.NONE;
        }

        @Override
        public CollapsibleNodeType visitStatement(Statement node) {
            return CollapsibleNodeType.STATEMENT;
        }
    }

    private static enum CollapsibleNodeType {
        CLASS_MEMEBER, TOP_LEVEL_DECL, STATEMENT, DIRECTIVE, NAMESPACE_DIRECTIVE, NONE;
    }

    /**
     * Matches comments.
     */
    private static 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 class CommentPosition extends Position implements IProjectionPosition {

        CommentPosition(int offset, int length) {
            super(offset, length);
        }

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

        @Override
        public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
            DocumentCharacterIterator sequence;
            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
         * @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(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 Dart element, one folding away any lines after the
     * caption.
     */
    private static class DartElementPosition extends Position implements IProjectionPosition {

        private AstNode fMember;

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

        @Override
        public int computeCaptionOffset(IDocument document) throws BadLocationException {
            int nameStart = offset;
            SourceRange nameRange = new SourceRange(fMember.getOffset(), fMember.getLength());
            if (nameRange != null) {
                nameStart = nameRange.getOffset();
            }
            return nameStart - offset;
        }

        @Override
        public IRegion[] computeProjectionRegions(IDocument document) throws BadLocationException {
            int nameStart = offset;
            SourceRange nameRange = new SourceRange(fMember.getOffset(), fMember.getLength());
            if (nameRange != null) {
                nameStart = nameRange.getOffset();
            }
            int firstLine = document.getLineOfOffset(offset);
            int captionLine = document.getLineOfOffset(nameStart);
            int lastLine = document.getLineOfOffset(offset + length);
            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(AstNode member) {
            Assert.isNotNull(member);
            fMember = member;
        }

    }

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

    private static class InvalidSourceException extends Exception {
    }

    /**
     * Matches members.
     */
    private static class MemberFilter extends CollapsibleNodeClassifier implements Filter {

        @Override
        public boolean match(DartProjectionAnnotation annotation) {
            if (!annotation.isComment() && !annotation.isMarkedDeleted()) {
                AstNode element = annotation.getElement();
                return element.accept(this) != CollapsibleNodeType.NONE;
            }
            return false;
        }

    }

    /**
     * Internal projection listener.
     */
    private 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 {
        Token firstToken;
        Token currentToken;
        int begin;

        TokenStream(String source) throws InvalidSourceException {
            BooleanErrorListener listener = new BooleanErrorListener();
            Scanner scanner = new Scanner(null, new CharSequenceReader(source), listener);
            if (listener.getErrorReported()) {
                throw new InvalidSourceException();
            } else {
                firstToken = scanner.tokenize();
                currentToken = firstToken;
                begin = 0;
            }
        }

        void begin(int start) throws InvalidSourceException {
            if (start == begin) {
                return;
            }
            if (start < begin) {
                begin = 0;
                currentToken = firstToken;
            }
            Token prev = currentToken;
            while (begin < start) {
                currentToken = currentToken.getNext();
                if (currentToken == prev) {
                    throw new InvalidSourceException();
                }
                prev = currentToken;
                begin = currentToken.getOffset();
            }
        }

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

    private static class Tuple {
        DartProjectionAnnotation annotation;
        Position position;

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

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

    /* context and listeners */
    private CompilationUnitEditor dartEditor;
    private ProjectionListener projectionListener;
    private AstNode fInput;

    /* preferences */
    private boolean collapseDartDoc = false;
    private boolean collapseImportContainer = false;
    private boolean collapseMembers = false;
    private boolean collapseHeaderComments = true;
    private boolean collapseClasses = false;
    private boolean collapseFunctions = false;

    /* filters */
    /** Member filter, matches collapse-able AST nodes. */
    private Filter memberFilter = new MemberFilter();
    /** Comment filter, matches comments. */
    private Filter commentFilter = new CommentFilter();

    private CollapsibleNodeClassifier classifier = new CollapsibleNodeClassifier();
    private IDartReconcilingListener reconcileListener = new IDartReconcilingListener() {
        @Override
        public void reconciled(CompilationUnit unit) {
            refresh();
        }
    };

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

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

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

    @Override
    public void initialize() {
        update(createInitialContext());
    }

    @Override
    public void install(ITextEditor editor, ProjectionViewer viewer) {
        Assert.isLegal(editor != null);
        Assert.isLegal(viewer != null);
        internalUninstall();
        if (editor instanceof CompilationUnitEditor) {
            projectionListener = new ProjectionListener(viewer);
            dartEditor = (CompilationUnitEditor) editor;
            dartEditor.addReconcileListener(reconcileListener);
        }
    }

    public void refresh() {
        final FoldingStructureComputationContext context = createContext(false);

        Display.getDefault().asyncExec(new Runnable() {
            @Override
            public void run() {
                update(context);
            }
        });
    }

    @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 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);
                //        if (endOffset != region.getOffset() + region.getLength()) {
                endOffset = document.getLineOffset(end + 1);
                //        }
            } else {
                endOffset = document.getLineOffset(end);
                if (endOffset != end) {
                    endOffset += document.getLineLength(end);
                }
            }
            return new Region(offset, endOffset - offset);
        } catch (BadLocationException x) {
            // concurrent modification
            return null;
        }
    }

    /**
     * Computes the folding structure for a given {@link AstNode Dart node}. Computed projection
     * annotations are
     * {@link DartFoldingStructureProvider.FoldingStructureComputationContext#addProjectionRange(DartFoldingStructureProvider.DartProjectionAnnotation, Position)
     * added} to the computation context.
     * <p>
     * This implementation creates projection annotations for the following elements:
     * <ul>
     * <li>top-level functions, variables, and typedefs</li>
     * <li>classes</li>
     * <li>fields and methods</li>
     * <li>local functions</li>
     * </ul>
     * </p>
     * 
     * @param node the Dart AST node to compute the folding structure for
     * @param ctx the computation context
     */
    protected void computeFoldingStructure(AstNode node, FoldingStructureComputationContext ctx) {

        boolean collapse = false;
        boolean collapseCode = true;
        switch (node.accept(classifier)) {
        case DIRECTIVE:
            collapse = ctx.collapseImportContainer();
            break;
        case TOP_LEVEL_DECL:
            collapse = ctx.collapseClasses();
            break;
        case CLASS_MEMEBER:
            collapse = ctx.collapseMembers();
            break;
        case STATEMENT:
            collapse = ctx.collapseStatements();
        default:
            return;
        }

        IRegion[] regions = computeProjectionRanges(node, 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()) && node == ctx.getFirstRef()) {
                            commentCollapse = ctx.collapseHeaderComments();
                        } else {
                            commentCollapse = ctx.collapseDartDoc();
                        }
                        ctx.addProjectionRange(new DartProjectionAnnotation(commentCollapse, node, true), position);
                    }
                }
            }
            // code
            if (collapseCode) {
                IRegion normalized = alignRegion(regions[regions.length - 1], ctx);
                if (normalized != null) {
                    Position position = node instanceof ExecutableElement ? createMemberPosition(normalized, node)
                            : createCommentPosition(normalized);
                    if (position != null) {
                        ctx.addProjectionRange(new DartProjectionAnnotation(collapse, node, false), position);
                    }
                }
            }
        }
    }

    /**
     * Computes the projection ranges for a given <code>ASTNode</code>. More than one range or none at
     * all may be returned. If there are no fold-able regions, an empty array is returned.
     * <p>
     * The last region in the returned array (if not empty) describes the region for the Dart node
     * 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 IRegion[] computeProjectionRanges(AstNode reference, FoldingStructureComputationContext ctx) {
        SourceRange range = new SourceRange(reference.getOffset(), reference.getLength());
        if (!isAvailable(range)) {
            return new IRegion[0];
        }
        List<IRegion> regions = new ArrayList<IRegion>();
        if (!ctx.hasFirstRef()) {
            ctx.setFirstRef(reference);
            IRegion headerComment = computeHeaderComment(ctx);
            if (headerComment != null) {
                regions.add(headerComment);
                ctx.setHasHeaderComment();
            }
        }
        int shift = range.getOffset();
        TokenStream scanner;
        try {
            scanner = ctx.getScanner(shift);
        } catch (InvalidSourceException ex) {
            return new IRegion[0];
        }
        int start = shift;
        Token token = scanner.next();
        start = token.getOffset();
        Token comment = token.getPrecedingComments();
        if (dartEditor == null) {
            return new IRegion[0];
        }
        IEditorInput editorInput = dartEditor.getEditorInput();
        IDocumentProvider documentProvider = dartEditor.getDocumentProvider();
        IDocument doc = documentProvider.getDocument(editorInput);
        while (comment != null) {
            int s = comment.getOffset();
            int l = comment.getLength();
            if (comment.getLexeme().startsWith("//")) {
                Token nextComment = comment.getNext();
                while (nextComment != null && nextComment != token && nextComment.getLexeme().startsWith("//")) {
                    l = nextComment.getEnd() - s;
                    nextComment = nextComment.getNext();
                }
                comment = nextComment;
            } else {
                comment = comment.getNext();
            }
            if (isSpanningMultipleLines(doc, s, l)) {
                regions.add(new Region(s, l));
            }
            if (comment == token) {
                comment = null;
            }
        }
        int len = shift + range.getLength() - start - 1;
        regions.add(new Region(start, len));
        IRegion[] result = new IRegion[regions.size()];
        regions.toArray(result);
        return result;
    }

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

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

    /**
     * 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.
     */
    protected void handleProjectionDisabled() {
        dartEditor.removeReconcileListener(reconcileListener);
    }

    /**
     * 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}.
     */
    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();
        // TODO (danrubel) fix for use with analysis server
        if (isInstalled() && !DartCoreDebug.ENABLE_ANALYSIS_SERVER) {
            initialize();
            dartEditor.addReconcileListener(reconcileListener);
        }
    }

    /**
     * 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 boolean isInstalled() {
        return dartEditor != null;
    }

    private Map<AstNode, List<Tuple>> computeCurrentStructure(FoldingStructureComputationContext ctx) {
        Map<AstNode, List<Tuple>> map = new HashMap<AstNode, List<Tuple>>();
        ProjectionAnnotationModel model = ctx.getModel();
        @SuppressWarnings("unchecked")
        Iterator<Object> e = model.getAnnotationIterator();
        while (e.hasNext()) {
            Object annotation = e.next();
            if (annotation instanceof DartProjectionAnnotation) {
                DartProjectionAnnotation projection = (DartProjectionAnnotation) annotation;
                Position position = model.getPosition(projection);
                Assert.isNotNull(position);
                List<Tuple> list = map.get(projection.getElement());
                if (list == null) {
                    list = new ArrayList<Tuple>(2);
                    map.put(projection.getElement(), list);
                }
                list.add(new Tuple(projection, position));
            }
        }
        Comparator<Tuple> comparator = new Comparator<Tuple>() {
            @Override
            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;
    }

    private void computeFoldingStructure(final FoldingStructureComputationContext ctx) {
        CompilationUnit parent = getInputElement();
        try {
            if (parent == null) {
                return;
            }
            String source = ctx.getDocument().get();
            if (source == null) {
                return;
            }
            ctx.setScannerSource(source);
            BreadthFirstVisitor<Void> v = new BreadthFirstVisitor<Void>() {
                @Override
                public Void visitNode(AstNode node) {
                    computeFoldingStructure(node, ctx);
                    super.visitNode(node);
                    return null;
                }
            };
            v.visitAllNodes(parent);
        } catch (InvalidSourceException x) {
            DartToolsPlugin.log(x);
        }
    }

    private IRegion computeHeaderComment(FoldingStructureComputationContext ctx) {
        // search at most up to the first element
        TokenStream scanner;
        try {
            scanner = ctx.getScanner(0);
        } catch (InvalidSourceException ex) {
            return null;
        }
        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;
            Token nextComment = comment.getNext();
            if (nextComment == terminal || nextComment == null) {
                comment = null;
            } else if (nextComment.getOffset() != comment.getEnd()) {
                comment = null;
            } else {
                comment = nextComment;
            }
        }
        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>DartProjectionAnnotation</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<DartProjectionAnnotation> annotations,
            Map<DartProjectionAnnotation, Position> positionMap, FoldingStructureComputationContext ctx) {
        Iterator<DartProjectionAnnotation> it = annotations.iterator();
        while (it.hasNext()) {
            DartProjectionAnnotation annotation = 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 CompilationUnit getInputElement() {
        if (dartEditor == null) {
            return null;
        }
        return dartEditor.getInputUnit();
    }

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

    private void initializePreferences() {
        IPreferenceStore store = DartToolsPlugin.getDefault().getPreferenceStore();//dartEditor.getPreferences()
        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);
        collapseClasses = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_CLASSES);
        collapseFunctions = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_FUNCTIONS);
    }

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

    private boolean isSpanningMultipleLines(IDocument doc, int offset, int length) {
        try {
            return doc.getLineOfOffset(offset + length) - doc.getLineOfOffset(offset) > 1;
        } catch (BadLocationException ex) {
            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<DartProjectionAnnotation> deletions, Map<DartProjectionAnnotation, Position> additions,
            List<DartProjectionAnnotation> changes, FoldingStructureComputationContext ctx) {
        if (deletions.isEmpty() || (additions.isEmpty() && changes.isEmpty())) {
            return;
        }
        List<DartProjectionAnnotation> newDeletions = new ArrayList<DartProjectionAnnotation>();
        List<DartProjectionAnnotation> newChanges = new ArrayList<DartProjectionAnnotation>();
        Iterator<DartProjectionAnnotation> deletionIterator = deletions.iterator();
        while (deletionIterator.hasNext()) {
            DartProjectionAnnotation deleted = deletionIterator.next();
            Position deletedPosition = ctx.getModel().getPosition(deleted);
            if (deletedPosition == null) {
                continue;
            }
            Tuple deletedTuple = new Tuple(deleted, deletedPosition);
            Tuple match = findMatch(deletedTuple, changes, null, ctx);
            boolean addToDeletions = true;
            if (match == null) {
                match = findMatch(deletedTuple, additions.keySet(), additions, ctx);
                addToDeletions = false;
            }
            if (match != null) {
                AstNode element = match.annotation.getElement();
                deleted.setElement(element);
                deletedPosition.setLength(match.position.getLength());
                if (deletedPosition instanceof DartElementPosition && element instanceof AstNode) {
                    DartElementPosition jep = (DartElementPosition) deletedPosition;
                    jep.setMember(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<DartProjectionAnnotation> modified = new ArrayList<DartProjectionAnnotation>();
        @SuppressWarnings("unchecked")
        Iterator<Object> iter = model.getAnnotationIterator();
        while (iter.hasNext()) {
            Object annotation = iter.next();
            if (annotation instanceof DartProjectionAnnotation) {
                DartProjectionAnnotation projection = (DartProjectionAnnotation) annotation;
                if (expand == projection.isCollapsed() && filter.match(projection)) {
                    if (expand) {
                        projection.markExpanded();
                    } else {
                        projection.markCollapsed();
                    }
                    modified.add(projection);
                }
            }
        }
        model.modifyAnnotations(null, null, modified.toArray(new Annotation[modified.size()]));
    }

    private void update(FoldingStructureComputationContext ctx) {
        if (ctx == null) {
            return;
        }
        if (DartUI.isTooComplexDartDocument(ctx.getDocument())) {
            return;
        }
        Map<DartProjectionAnnotation, Position> additions = new HashMap<DartProjectionAnnotation, Position>();
        List<DartProjectionAnnotation> deletions = new ArrayList<DartProjectionAnnotation>();
        List<DartProjectionAnnotation> updates = new ArrayList<DartProjectionAnnotation>();
        computeFoldingStructure(ctx);
        Map<DartProjectionAnnotation, Position> newStructure = ctx.map;
        Map<AstNode, List<Tuple>> oldStructure = computeCurrentStructure(ctx);
        Iterator<DartProjectionAnnotation> e = newStructure.keySet().iterator();
        while (e.hasNext()) {
            DartProjectionAnnotation newAnnotation = e.next();
            Position newPosition = newStructure.get(newAnnotation);
            AstNode element = newAnnotation.getElement();
            List<Tuple> annotations = oldStructure.get(element);
            if (annotations == null) {
                additions.put(newAnnotation, newPosition);
            } else {
                Iterator<Tuple> x = annotations.iterator();
                boolean matched = false;
                while (x.hasNext()) {
                    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 (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);
                }
            }
        }
        Iterator<List<Tuple>> e2 = oldStructure.values().iterator();
        while (e2.hasNext()) {
            List<Tuple> list = e2.next();
            int size = list.size();
            for (int i = 0; i < size; i++) {
                deletions.add(list.get(i).annotation);
            }
        }
        match(deletions, additions, updates, ctx);
        Annotation[] deletedArray = deletions.toArray(new Annotation[deletions.size()]);
        Annotation[] changedArray = updates.toArray(new Annotation[updates.size()]);
        ctx.getModel().modifyAnnotations(deletedArray, additions, changedArray);
        try {
            ctx.setScannerSource(""); // clear token stream
        } catch (InvalidSourceException e1) {
        }
    }
}