blob: c6868450e6514058dbadabd5a58d084357e932b3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*
*******************************************************************************/
package org.eclipse.wst.sse.ui.internal.projection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcileStep;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.projection.IProjectionListener;
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.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy;
import org.eclipse.wst.sse.ui.internal.reconcile.StructuredReconcileStep;
/**
* <p>This class has the default implementation for a structured editor folding strategy.
* Each content type that the structured editor supports should create an extension point
* specifying a child class of this class as its folding strategy, if that content type
* should have folding.</p>
*
* <p>EX:<br />
* <code>&lt;extension point="org.eclipse.wst.sse.ui.editorConfiguration"&gt;<br />
* &lt;provisionalConfiguration<br />
* type="foldingstrategy"<br />
* class="org.eclipse.wst.xml.ui.internal.projection.XMLFoldingStrategy"<br />
* target="org.eclipse.core.runtime.xml, org.eclipse.wst.xml.core.xmlsource" /&gt;<br />
* &lt;/extension&gt;</code></p>
*
* <p>Different content types can use the same folding strategy if it makes sense to do so,
* such as with HTML/XML/JSP.</p>
*
* <p>This strategy is based on the Reconciler paradigm and thus runs in the background,
* this means that even for very large documents requiring the calculation of 1000s of
* folding annotations the user will not be effected except for the annotations may take
* some time to first appear.</p>
*/
public abstract class AbstractStructuredFoldingStrategy
extends AbstractStructuredTextReconcilingStrategy implements IProjectionListener {
/**
* The org.eclipse.wst.sse.ui.editorConfiguration provisionalConfiguration type
*/
public static final String ID = "foldingstrategy"; //$NON-NLS-1$
/**
* A named preference that controls whether folding is enabled in the
* Structured Text editor.
*/
public final static String FOLDING_ENABLED = "foldingEnabled"; //$NON-NLS-1$
/**
* The annotation model associated with this folding strategy
*/
protected ProjectionAnnotationModel fProjectionAnnotationModel;
/**
* The structured text viewer this folding strategy is associated with
*/
private ProjectionViewer fViewer;
/**
* these are not used but needed to implement abstract methods
*/
private IReconcileStep fFoldingStep;
/**
* Default constructor for the folding strategy, can not take any parameters
* because subclasses instances of this class are created using reflection
* based on plugin settings
*/
public AbstractStructuredFoldingStrategy() {
super();
}
/**
* The folding strategy must be associated with a viewer for it to function
*
* @param viewer the viewer to associate this folding strategy with
*/
public void setViewer(ProjectionViewer viewer) {
super.setViewer(viewer);
if(fViewer != null) {
fViewer.removeProjectionListener(this);
}
fViewer = viewer;
fViewer.addProjectionListener(this);
fProjectionAnnotationModel = fViewer.getProjectionAnnotationModel();
}
public void uninstall() {
setDocument(null);
if(fViewer != null) {
fViewer.removeProjectionListener(this);
fViewer = null;
}
fFoldingStep = null;
projectionDisabled();
}
/*
* (non-Javadoc)
* @see org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
*/
public void setDocument(IDocument document) {
super.setDocument(document);
}
/*
* (non-Javadoc)
* @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionDisabled()
*/
public void projectionDisabled() {
fProjectionAnnotationModel = null;
}
/*
* (non-Javadoc)
* @see org.eclipse.jface.text.source.projection.IProjectionListener#projectionEnabled()
*/
public void projectionEnabled() {
if(fViewer != null) {
fProjectionAnnotationModel = fViewer.getProjectionAnnotationModel();
}
}
/**
* <p><b>NOTE 1:</b> This implementation of reconcile ignores the given {@link IRegion} and instead gets all of the
* structured document regions effected by the range of the given {@link DirtyRegion}.</p>
*
* <p><b>NOTE 2:</b> In cases where multiple {@link IRegion} maybe dirty it is more efficient to pass one
* {@link DirtyRegion} contain all of the {@link IRegion}s then one {@link DirtyRegion} for each IRegion.
* Case in point, when processing the entire document it is <b>recommended</b> that this function be
* called only <b>once</b> with one {@link DirtyRegion} that spans the entire document.</p>
*
* @param dirtyRegion the region that needs its folding annotations processed
* @param subRegion ignored
*
* @see org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion, org.eclipse.jface.text.IRegion)
*/
public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
IStructuredModel model = null;
if(fProjectionAnnotationModel != null) {
try {
model = StructuredModelManager.getModelManager().getExistingModelForRead(getDocument());
if(model != null) {
//use the structured doc to get all of the regions effected by the given dirty region
IStructuredDocument structDoc = model.getStructuredDocument();
IStructuredDocumentRegion[] structRegions = structDoc.getStructuredDocumentRegions(dirtyRegion.getOffset(), dirtyRegion.getLength());
Set indexedRegions = getIndexedRegions(model, structRegions);
//these are what are passed off to the annotation model to
//actually create and maintain the annotations
List modifications = new ArrayList();
List deletions = new ArrayList();
Map additions = new HashMap();
boolean isInsert = dirtyRegion.getType().equals(DirtyRegion.INSERT);
boolean isRemove = dirtyRegion.getType().equals(DirtyRegion.REMOVE);
//find and mark all folding annotations with length 0 for deletion
markInvalidAnnotationsForDeletion(dirtyRegion, deletions);
//reconcile each effected indexed region
Iterator indexedRegionsIter = indexedRegions.iterator();
while(indexedRegionsIter.hasNext() && fProjectionAnnotationModel != null) {
IndexedRegion indexedRegion = (IndexedRegion)indexedRegionsIter.next();
//only try to create an annotation if the index region is a valid type
if(indexedRegionValidType(indexedRegion)) {
FoldingAnnotation annotation = new FoldingAnnotation(indexedRegion, false);
// if INSERT calculate new addition position or modification
// else if REMOVE add annotation to the deletion list
if(isInsert) {
Annotation existingAnno = getExistingAnnotation(indexedRegion);
//if projection has been disabled the iter could be null
//if annotation does not already exist for this region create a new one
//else modify an old one, which could include deletion
if(existingAnno == null) {
Position newPos = calcNewFoldPosition(indexedRegion);
if(newPos != null && newPos.length > 0) {
additions.put(annotation, newPos);
}
} else {
updateAnnotations(existingAnno, indexedRegion, additions, modifications, deletions);
}
} else if (isRemove) {
deletions.add(annotation);
}
}
}
//be sure projection has not been disabled
if(fProjectionAnnotationModel != null) {
//send the calculated updates to the annotations to the annotation model
fProjectionAnnotationModel.modifyAnnotations((Annotation[])deletions.toArray(new Annotation[1]), additions, (Annotation[])modifications.toArray(new Annotation[0]));
}
}
} finally {
if(model != null) {
model.releaseFromRead();
}
}
}
}
/**
* <p>Every implementation of the folding strategy calculates the position for a given
* IndexedRegion differently. Also this calculation often relies on casting to internal classes
* only available in the implementing classes plugin</p>
*
* @param indexedRegion the IndexedRegion to calculate a new annotation position for
* @return the calculated annotation position or NULL if none can be calculated based on the given region
*/
abstract protected Position calcNewFoldPosition(IndexedRegion indexedRegion);
/**
* This is the default behavior for updating a dirtied IndexedRegion. This function
* can be overridden if slightly different functionality is required in a specific instance
* of this class.
*
* @param existingAnnotationsIter the existing annotations that need to be updated
* based on the given dirtied IndexRegion
* @param dirtyRegion the IndexedRegion that caused the annotations need for updating
* @param modifications the list of annotations to be modified
* @param deletions the list of annotations to be deleted
*/
protected void updateAnnotations(Annotation existingAnnotation, IndexedRegion dirtyRegion, Map additions, List modifications, List deletions) {
if(existingAnnotation instanceof FoldingAnnotation) {
FoldingAnnotation foldingAnnotation = (FoldingAnnotation)existingAnnotation;
Position newPos = calcNewFoldPosition(foldingAnnotation.getRegion());
//if a new position can be calculated then update the position of the annotation,
//else the annotation needs to be deleted
if(newPos != null && newPos.length > 0 && fProjectionAnnotationModel != null) {
Position oldPos = fProjectionAnnotationModel.getPosition(foldingAnnotation);
//only update the position if we have to
if(!newPos.equals(oldPos)) {
oldPos.setOffset(newPos.offset);
oldPos.setLength(newPos.length);
modifications.add(foldingAnnotation);
}
} else {
deletions.add(foldingAnnotation);
}
}
}
/**
* <p>Searches the given {@link DirtyRegion} for annotations that now have a length of 0.
* This is caused when something that was being folded has been deleted. These {@link FoldingAnnotation}s
* are then added to the {@link List} of {@link FoldingAnnotation}s to be deleted</p>
*
* @param dirtyRegion find the now invalid {@link FoldingAnnotation}s in this {@link DirtyRegion}
* @param deletions the current list of {@link FoldingAnnotation}s marked for deletion that the
* newly found invalid {@link FoldingAnnotation}s will be added to
*/
protected void markInvalidAnnotationsForDeletion(DirtyRegion dirtyRegion, List deletions) {
Iterator iter = getAnnotationIterator(dirtyRegion);
while(iter.hasNext()) {
Annotation anno = (Annotation)iter.next();
if(anno instanceof FoldingAnnotation) {
Position pos = fProjectionAnnotationModel.getPosition(anno);
if(pos.length == 0) {
deletions.add(anno);
}
}
}
}
/**
* Should return true if the given IndexedRegion is one that this strategy pays attention to
* when calculating new and updated annotations
*
* @param indexedRegion the IndexedRegion to check the type of
* @return true if the IndexedRegion is of a valid type, false otherwise
*/
abstract protected boolean indexedRegionValidType(IndexedRegion indexedRegion);
/**
* Steps are not used in this strategy
*
* @see org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy#containsStep(org.eclipse.jface.text.reconciler.IReconcileStep)
*/
protected boolean containsStep(IReconcileStep step) {
return fFoldingStep.equals(step);
}
/**
* Steps are not used in this strategy
*
* @see org.eclipse.wst.sse.ui.internal.reconcile.AbstractStructuredTextReconcilingStrategy#createReconcileSteps()
*/
public void createReconcileSteps() {
fFoldingStep = new StructuredReconcileStep() { };
}
/**
* A FoldingAnnotation is a ProjectionAnnotation in a structured document.
* Its extended functionality include storing the <code>IndexedRegion</code> it is folding
* and overriding the paint method (in a hacky type way) to prevent one line folding
* annotations to be drawn.
*/
protected class FoldingAnnotation extends ProjectionAnnotation {
private boolean fIsVisible; /* workaround for BUG85874 */
/**
* The IndexedRegion this annotation is folding
*/
private IndexedRegion fRegion;
/**
* Creates a new FoldingAnnotation that is associated with the given IndexedRegion
*
* @param region the IndexedRegion this annotation is associated with
* @param isCollapsed true if this annotation should be collapsed, false otherwise
*/
public FoldingAnnotation(IndexedRegion region, boolean isCollapsed) {
super(isCollapsed);
fIsVisible = false;
fRegion = region;
}
/**
* Returns the IndexedRegion this annotation is associated with
*
* @return the IndexedRegion this annotation is associated with
*/
public IndexedRegion getRegion() {
return fRegion;
}
public void setRegion(IndexedRegion region) {
fRegion = region;
}
/**
* Does not paint hidden annotations. Annotations are hidden when they
* only span one line.
*
* @see ProjectionAnnotation#paint(org.eclipse.swt.graphics.GC,
* org.eclipse.swt.widgets.Canvas,
* org.eclipse.swt.graphics.Rectangle)
*/
public void paint(GC gc, Canvas canvas, Rectangle rectangle) {
/* workaround for BUG85874 */
/*
* only need to check annotations that are expanded because hidden
* annotations should never have been given the chance to
* collapse.
*/
if (!isCollapsed()) {
// working with rectangle, so line height
FontMetrics metrics = gc.getFontMetrics();
if (metrics != null) {
// do not draw annotations that only span one line and
// mark them as not visible
if ((rectangle.height / metrics.getHeight()) <= 1) {
fIsVisible = false;
return;
}
}
}
fIsVisible = true;
super.paint(gc, canvas, rectangle);
}
/**
* @see org.eclipse.jface.text.source.projection.ProjectionAnnotation#markCollapsed()
*/
public void markCollapsed() {
/* workaround for BUG85874 */
// do not mark collapsed if annotation is not visible
if (fIsVisible)
super.markCollapsed();
}
/**
* Two FoldingAnnotations are equal if their IndexedRegions are equal
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj) {
boolean equal = false;
if(obj instanceof FoldingAnnotation) {
equal = fRegion.equals(((FoldingAnnotation)obj).fRegion);
}
return equal;
}
/**
* Returns the hash code of the IndexedRegion this annotation is associated with
*
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return fRegion.hashCode();
}
/**
* Returns the toString of the aIndexedRegion this annotation is associated with
*
* @see java.lang.Object#toString()
*/
public String toString() {
return fRegion.toString();
}
}
/**
* Given a {@link DirtyRegion} returns an {@link Iterator} of the already existing
* annotations in that region.
*
* @param dirtyRegion the {@link DirtyRegion} to check for existing annotations in
*
* @return an {@link Iterator} over the annotations in the given {@link DirtyRegion}.
* The iterator could have no annotations in it. Or <code>null</code> if projection has
* been disabled.
*/
private Iterator getAnnotationIterator(DirtyRegion dirtyRegion) {
Iterator annoIter = null;
//be sure project has not been disabled
if(fProjectionAnnotationModel != null) {
//workaround for Platform Bug 299416
int offset = dirtyRegion.getOffset();
if(offset > 0) {
offset--;
}
annoIter = fProjectionAnnotationModel.getAnnotationIterator(offset, dirtyRegion.getLength(), false, false);
}
return annoIter;
}
/**
* <p>Gets the first {@link Annotation} at the start offset of the given {@link IndexedRegion}.</p>
*
* @param indexedRegion get the first {@link Annotation} at this {@link IndexedRegion}
* @return the first {@link Annotation} at the start offset of the given {@link IndexedRegion}
*/
private Annotation getExistingAnnotation(IndexedRegion indexedRegion) {
Iterator iter = fProjectionAnnotationModel.getAnnotationIterator(indexedRegion.getStartOffset(), 1, false, true);
Annotation anno = null;
if(iter.hasNext()) {
anno = (Annotation)iter.next();
}
return anno;
}
/**
* <p>Gets all of the {@link IndexedRegion}s from the given {@link IStructuredModel} spand by the given
* {@link IStructuredDocumentRegion}s.</p>
*
* @param model the {@link IStructuredModel} used to get the {@link IndexedRegion}s
* @param structRegions get the {@link IndexedRegion}s spanned by these {@link IStructuredDocumentRegion}s
* @return the {@link Set} of {@link IndexedRegion}s from the given {@link IStructuredModel} spaned by the
* given {@link IStructuredDocumentRegion}s.
*/
private Set getIndexedRegions(IStructuredModel model, IStructuredDocumentRegion[] structRegions) {
Set indexedRegions = new HashSet();
//for each text region in each struct doc region find the indexed region it spans/is in
for(int structRegionIndex = 0; structRegionIndex < structRegions.length && fProjectionAnnotationModel != null; ++structRegionIndex) {
ITextRegionList textRegions = structRegions[structRegionIndex].getRegions();
for(int textRegionIndex = 0; textRegionIndex < textRegions.size() && fProjectionAnnotationModel != null; ++textRegionIndex) {
int offset = structRegions[structRegionIndex].getStartOffset(textRegions.get(textRegionIndex));
indexedRegions.add(model.getIndexedRegion(offset));
}
}
return indexedRegions;
}
}