blob: 201aa5b364aec76b367ffcc50aa6097562ff4d14 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Angelo Zerr <angelo.zerr@gmail.com> - adapt code org.eclipse.wst.sse.ui.internal.projection.AbstractStructuredFoldingStrategy to support generic indent folding strategy.
* [generic editor] Default Code folding for generic editor should use IndentFoldingStrategy - Bug 520659
*/
package org.eclipse.ui.internal.genericeditor.folding;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IProgressMonitor;
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.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
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;
/**
* Indent folding strategy to fold code by using indentation. The folding
* strategy must be associated with a viewer for it to function.
*/
public class IndentFoldingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension, IProjectionListener {
private IDocument document;
private ProjectionViewer viewer;
private ProjectionAnnotationModel projectionAnnotationModel;
private final String lineStartsWithKeyword;
public IndentFoldingStrategy() {
this(null);
}
public IndentFoldingStrategy(String lineStartsWithKeyword) {
this.lineStartsWithKeyword = lineStartsWithKeyword;
}
/**
* A FoldingAnnotation is a {@link ProjectionAnnotation} 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 visible; /* workaround for BUG85874 */
/**
* Creates a new FoldingAnnotation.
*
* @param isCollapsed true if this annotation should be collapsed, false
* otherwise
*/
public FoldingAnnotation(boolean isCollapsed) {
super(isCollapsed);
visible = false;
}
/**
* 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)
*/
@Override
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) {
visible = false;
return;
}
}
}
visible = true;
super.paint(gc, canvas, rectangle);
}
@Override
public void markCollapsed() {
/* workaround for BUG85874 */
// do not mark collapsed if annotation is not visible
if (visible)
super.markCollapsed();
}
}
/**
* 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) {
if (this.viewer != null) {
this.viewer.removeProjectionListener(this);
}
this.viewer = viewer;
this.viewer.addProjectionListener(this);
this.projectionAnnotationModel = this.viewer.getProjectionAnnotationModel();
}
public void uninstall() {
setDocument(null);
if (viewer != null) {
viewer.removeProjectionListener(this);
viewer = null;
}
projectionDisabled();
}
@Override
public void setDocument(IDocument document) {
this.document = document;
}
@Override
public void projectionDisabled() {
projectionAnnotationModel = null;
}
@Override
public void projectionEnabled() {
if (viewer != null) {
projectionAnnotationModel = viewer.getProjectionAnnotationModel();
}
}
private static class LineIndent {
public int line;
public final int indent;
public LineIndent(int line, int indent) {
this.line = line;
this.indent = indent;
}
}
@Override
public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
if (projectionAnnotationModel != null) {
// these are what are passed off to the annotation model to
// actually create and maintain the annotations
List<Annotation> modifications = new ArrayList<>();
List<FoldingAnnotation> deletions = new ArrayList<>();
List<FoldingAnnotation> existing = new ArrayList<>();
Map<Annotation, Position> additions = new HashMap<>();
// find and mark all folding annotations with length 0 for deletion
markInvalidAnnotationsForDeletion(dirtyRegion, deletions, existing);
List<LineIndent> previousRegions = new ArrayList<>();
int tabSize = 1;
int minimumRangeSize = 1;
try {
// Today we recompute annotation from the whole document each
// time.
// performance s good even with large document, but it should be
// better to loop for only DirtyRegion (and before/after)
// int offset = dirtyRegion.getOffset();
// int length = dirtyRegion.getLength();
// int startLine = 0; //document.getLineOfOffset(offset);
int endLine = document.getNumberOfLines() - 1; // startLine +
// document.getNumberOfLines(offset,
// length) - 1;
// sentinel, to make sure there's at least one entry
previousRegions.add(new LineIndent(endLine + 1, -1));
int lastLineWhichIsNotEmpty = 0;
int lineEmptyCount = 0;
Integer lastLineForKeyword = null;
int line = endLine;
for (line = endLine; line >= 0; line--) {
int lineOffset = document.getLineOffset(line);
String delim = document.getLineDelimiter(line);
int lineLength = document.getLineLength(line) - (delim != null ? delim.length() : 0);
String lineContent = document.get(lineOffset, lineLength);
LineState state = getLineState(lineContent, lastLineForKeyword);
switch (state) {
case StartWithKeyWord:
lineEmptyCount = 0;
lastLineWhichIsNotEmpty = line;
if (lastLineForKeyword == null) {
lastLineForKeyword = line;
}
break;
case EmptyLine:
lineEmptyCount++;
break;
default:
addAnnotationForKeyword(modifications, deletions, existing, additions,
line + 1 + lineEmptyCount, lastLineForKeyword);
lastLineForKeyword = null;
lineEmptyCount = 0;
lastLineWhichIsNotEmpty = line;
int indent = computeIndentLevel(lineContent, tabSize);
if (indent == -1) {
continue; // only whitespace
}
LineIndent previous = previousRegions.get(previousRegions.size() - 1);
if (previous.indent > indent) {
// discard all regions with larger indent
do {
previousRegions.remove(previousRegions.size() - 1);
previous = previousRegions.get(previousRegions.size() - 1);
} while (previous.indent > indent);
// new folding range
int endLineNumber = previous.line - 1;
if (endLineNumber - line >= minimumRangeSize) {
updateAnnotation(modifications, deletions, existing, additions, line, endLineNumber);
}
}
if (previous.indent == indent) {
previous.line = line;
} else { // previous.indent < indent
// new region with a bigger indent
previousRegions.add(new LineIndent(line, indent));
}
}
}
addAnnotationForKeyword(modifications, deletions, existing, additions, lastLineWhichIsNotEmpty,
lastLineForKeyword);
} catch (BadLocationException e) {
// should never done
e.printStackTrace();
}
// be sure projection has not been disabled
if (projectionAnnotationModel != null) {
if (!existing.isEmpty()) {
deletions.addAll(existing);
}
// send the calculated updates to the annotations to the
// annotation model
projectionAnnotationModel.modifyAnnotations(deletions.toArray(new Annotation[1]), additions,
modifications.toArray(new Annotation[0]));
}
}
}
private void addAnnotationForKeyword(List<Annotation> modifications, List<FoldingAnnotation> deletions,
List<FoldingAnnotation> existing, Map<Annotation, Position> additions, int startLine,
Integer lastLineForKeyword) throws BadLocationException {
if (lastLineForKeyword != null) {
updateAnnotation(modifications, deletions, existing, additions, startLine, lastLineForKeyword);
}
}
private enum LineState {
StartWithKeyWord, DontStartWithKeyWord, EmptyLine
}
/**
* Returns the line state for line which starts with a given keyword.
*
* @param lineContent line content.
* @param lastLineForKeyword last line for the given keyword.
* @return
*/
private LineState getLineState(String lineContent, Integer lastLineForKeyword) {
if (lineStartsWithKeyword == null) {
// none keyword defined.
return LineState.DontStartWithKeyWord;
}
if (lineContent != null && lineContent.trim().startsWith(lineStartsWithKeyword)) {
// The line starts with the given keyword (ex: starts with "import")
return LineState.StartWithKeyWord;
}
if (lastLineForKeyword != null && (lineContent == null || lineContent.trim().isEmpty())) {
// a last line for keyword was defined, line is empty
return LineState.EmptyLine;
}
return LineState.DontStartWithKeyWord;
}
/**
* Compute indentation level of the given line by using the given tab size.
*
* @param line the line text.
* @param tabSize the tab size.
* @return the indentation level of the given line by using the given tab size.
*/
private static int computeIndentLevel(String line, int tabSize) {
int i = 0;
int indent = 0;
while (i < line.length()) {
char ch = line.charAt(i);
if (ch == ' ') {
indent++;
} else if (ch == '\t') {
indent = indent - indent % tabSize + tabSize;
} else {
break;
}
i++;
}
if (i == line.length()) {
return -1; // line only consists of whitespace
}
return indent;
}
/**
* 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<Annotation> getAnnotationIterator(DirtyRegion dirtyRegion) {
Iterator<Annotation> annoIter = null;
// be sure project has not been disabled
if (projectionAnnotationModel != null) {
// workaround for Platform Bug 299416
annoIter = projectionAnnotationModel.getAnnotationIterator(0, document.getLength(), false, false);
}
return annoIter;
}
/**
* Update annotations.
*
* @param modifications the folding annotations to update.
* @param deletions the folding annotations to delete.
* @param existing the existing folding annotations.
* @param additions annoation to add
* @param line the line index
* @param endLineNumber the end line number
* @throws BadLocationException
*/
private void updateAnnotation(List<Annotation> modifications, List<FoldingAnnotation> deletions,
List<FoldingAnnotation> existing, Map<Annotation, Position> additions, int line, Integer endLineNumber)
throws BadLocationException {
int startOffset = document.getLineOffset(line);
int endOffset = document.getLineOffset(endLineNumber) + document.getLineLength(endLineNumber);
Position newPos = new Position(startOffset, endOffset - startOffset);
if (!existing.isEmpty()) {
FoldingAnnotation existingAnnotation = existing.remove(existing.size() - 1);
updateAnnotations(existingAnnotation, newPos, modifications, deletions);
} else {
additions.put(new FoldingAnnotation(false), newPos);
}
}
/**
* Update annotations.
*
* @param existingAnnotation the existing annotations that need to be updated
* based on the given dirtied IndexRegion
* @param newPos the new position that caused the annotations need
* for updating and null otherwise.
* @param modifications the list of annotations to be modified
* @param deletions the list of annotations to be deleted
*/
protected void updateAnnotations(Annotation existingAnnotation, Position newPos, List<Annotation> modifications,
List<FoldingAnnotation> deletions) {
if (existingAnnotation instanceof FoldingAnnotation) {
FoldingAnnotation foldingAnnotation = (FoldingAnnotation) existingAnnotation;
// 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 && projectionAnnotationModel != null) {
Position oldPos = projectionAnnotationModel.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<FoldingAnnotation> deletions,
List<FoldingAnnotation> existing) {
Iterator<Annotation> iter = getAnnotationIterator(dirtyRegion);
if (iter != null) {
while (iter.hasNext()) {
Annotation anno = iter.next();
if (anno instanceof FoldingAnnotation) {
FoldingAnnotation folding = (FoldingAnnotation) anno;
Position pos = projectionAnnotationModel.getPosition(anno);
if (pos.length == 0) {
deletions.add(folding);
} else {
existing.add(folding);
}
}
}
}
}
@Override
public void reconcile(IRegion partition) {
// not used, we use:
// reconcile(DirtyRegion dirtyRegion, IRegion subRegion)
}
@Override
public void setProgressMonitor(IProgressMonitor monitor) {
// Do nothing
}
@Override
public void initialReconcile() {
reconcile(new DirtyRegion(0, document.getLength(), DirtyRegion.INSERT, document.get()), null);
}
}