/*
* 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<Annotation, Position></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) {
}
}
}