/*
* 10/08/2011
*
* Fold.java - A foldable region of text in an RSyntaxTextArea instance.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rsyntaxtextarea.folding;
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Position;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
/**
* Information about a foldable region.<p>
*
* A <code>Fold</code> has zero or more children, and <code>Folds</code> thus
* form a hierarchical structure, with "parent" folds containing the info about
* any "child" folds they contain.<p>
*
* Fold regions are denoted by a starting and ending offset, but the actual
* folding is done on a per-line basis, so <code>Fold</code> instances provide
* methods for retrieving both starting and ending offsets and lines. The
* starting and ending offsets/lines are "sticky" and correctly track their
* positions even as the document is modified.
*
* @author Robert Futrell
* @version 1.0
*/
public class Fold implements Comparable<Fold> {
private int type;
private RSyntaxTextArea textArea;
private Position startOffs;
private Position endOffs;
private Fold parent;
private List<Fold> children;
private boolean collapsed;
private int childCollapsedLineCount;
private int lastStartOffs = -1;
private int cachedStartLine;
private int lastEndOffs = -1;
private int cachedEndLine;
public Fold(int type, RSyntaxTextArea textArea, int startOffs)
throws BadLocationException {
this.type = type;
this.textArea = textArea;
this.startOffs = textArea.getDocument().createPosition(startOffs);
}
/**
* Creates a fold that is a child of this one.
*
* @param type The type of fold.
* @param startOffs The starting offset of the fold.
* @return The child fold.
* @throws BadLocationException If <code>startOffs</code> is invalid.
* @see FoldType
*/
public Fold createChild(int type, int startOffs) throws BadLocationException {
Fold child = new Fold(type, textArea, startOffs);
child.parent = this;
if (children==null) {
children = new ArrayList<Fold>();
}
children.add(child);
return child;
}
/**
* Two folds are considered equal if they start at the same offset.
*
* @param otherFold Another fold to compare this one to.
* @return How this fold compares to the other.
*/
public int compareTo(Fold otherFold) {
int result = -1;
if (otherFold!=null) {
result = startOffs.getOffset() - otherFold.startOffs.getOffset();
//result = getStartLine() - otherFold.getStartLine();
}
return result;
}
/**
* Returns whether the specified line would be hidden in this fold. Since
* RSTA displays the "first" line in a fold, this means that the line must
* must be between <code>(getStartLine()+1)</code> and
* <code>getEndLine()</code>, inclusive.
*
* @param line The line to check.
* @return Whether the line would be hidden if this fold is collapsed.
* @see #containsOffset(int)
* @see #containsOrStartsOnLine(int)
*/
public boolean containsLine(int line) {
return line>getStartLine() && line<=getEndLine();
}
/**
* Returns whether the given line is in the range
* <code>[getStartLine(), getEndLine()]</code>, inclusive.
*
* @param line The line to check.
* @return Whether this fold contains, or starts on, the line.
* @see #containsLine(int)
*/
public boolean containsOrStartsOnLine(int line) {
return line>=getStartLine() && line<=getEndLine();
}
/**
* Returns whether the specified offset is "inside" the fold. This method
* returns <code>true</code> if the offset is greater than the fold start
* offset, and no further than the last offset of the last folded line.
*
* @param offs The offset to check.
* @return Whether the offset is "inside" the fold.
* @see #containsLine(int)
*/
public boolean containsOffset(int offs) {
boolean contained = false;
if (offs>getStartOffset()) {
// Use Elements to avoid BadLocationExceptions
Element root = textArea.getDocument().getDefaultRootElement();
int line = root.getElementIndex(offs);
contained = line<=getEndLine();
}
return contained;
}
/**
* Two folds are considered equal if they have the same starting offset.
*
* @param otherFold Another fold to compare this one to.
* @return Whether the two folds are equal.
* @see #compareTo(Fold)
*/
@Override
public boolean equals(Object otherFold) {
return otherFold instanceof Fold && compareTo((Fold)otherFold)==0;
}
/**
* Returns a specific child fold.
*
* @param index The index of the child fold.
* @return The child fold.
* @see #getChildCount()
*/
public Fold getChild(int index) {
return children.get(index);
}
/**
* Returns the number of child folds.
*
* @return The number of child folds.
* @see #getChild(int)
*/
public int getChildCount() {
return children==null ? 0 : children.size();
}
/**
* Returns the array of child folds. This is a shallow copy.
*
* @return The array of child folds, or <code>null</code> if there are
* none.
*/
List<Fold> getChildren() {
return children;
}
/**
* Returns the number of collapsed lines under this fold. If this fold
* is collapsed, this method returns {@link #getLineCount()}, otherwise
* it returns the sum of all collapsed lines of all child folds of this
* one.<p>
*
* The value returned is cached, so this method returns quickly and
* shouldn't affect performance.
*
* @return The number of collapsed lines under this fold.
*/
public int getCollapsedLineCount() {
return collapsed ? getLineCount() : childCollapsedLineCount;
}
/**
* Returns the "deepest" fold containing the specified offset. It is
* assumed that it's already been verified that <code>offs</code> is indeed
* contained in this fold.
*
* @param offs The offset.
* @return The fold, or <code>null</code> if no child fold also contains
* the offset.
* @see FoldManager#getDeepestFoldContaining(int)
*/
Fold getDeepestFoldContaining(int offs) {
Fold deepestFold = this;
for (int i=0; i<getChildCount(); i++) {
Fold fold = getChild(i);
if (fold.containsOffset(offs)) {
deepestFold = fold.getDeepestFoldContaining(offs);
break;
}
}
return deepestFold;
}
/**
* Returns the "deepest" open fold containing the specified offset. It
* is assumed that it's already been verified that <code>offs</code> is
* indeed contained in this fold.
*
* @param offs The offset.
* @return The fold, or <code>null</code> if no open fold contains the
* offset.
* @see FoldManager#getDeepestOpenFoldContaining(int)
*/
Fold getDeepestOpenFoldContaining(int offs) {
Fold deepestFold = this;
for (int i=0; i<getChildCount(); i++) {
Fold fold = getChild(i);
if (fold.containsOffset(offs)) {
if (fold.isCollapsed()) {
break;
}
deepestFold = fold.getDeepestOpenFoldContaining(offs);
break;
}
}
return deepestFold;
}
/**
* Returns the end line of this fold. For example, in languages such as
* C and Java, this might be the line containing the closing curly brace of
* a code block.<p>
*
* The value returned by this method will automatically update as the
* text area's contents are modified, to track the ending line of the
* code block.
*
* @return The end line of this code block.
* @see #getEndOffset()
* @see #getStartLine()
*/
public int getEndLine() {
int endOffs = getEndOffset();
if (lastEndOffs==endOffs) {
return cachedEndLine;
}
lastEndOffs = endOffs;
Element root = textArea.getDocument().getDefaultRootElement();
return cachedEndLine = root.getElementIndex(endOffs);
}
/**
* Returns the end offset of this fold. For example, in languages such as
* C and Java, this might be the offset of the closing curly brace of a
* code block.<p>
*
* The value returned by this method will automatically update as the
* text area's contents are modified, to track the ending offset of the
* code block.
*
* @return The end offset of this code block, or {@link Integer#MAX_VALUE}
* if this fold region isn't closed properly. The latter causes
* this fold to collapsed all lines through the end of the file.
* @see #getEndLine()
* @see #getStartOffset()
*/
public int getEndOffset() {
return endOffs!=null ? endOffs.getOffset() : Integer.MAX_VALUE;
}
/**
* Returns the type of fold this is. This will be one of the values in
* {@link FoldType}, or a user-defined value.
*
* @return The type of fold this is.
*/
public int getFoldType() {
return type;
}
/**
* Returns whether this fold has any child folds.
*
* @return Whether this fold has any children.
* @see #getChildCount()
*/
public boolean getHasChildFolds() {
return getChildCount()>0;
}
/**
* Returns the last child fold.
*
* @return The last child fold, or <code>null</code> if this fold does not
* have any children.
* @see #getChild(int)
* @see #getHasChildFolds()
*/
public Fold getLastChild() {
int childCount = getChildCount();
return childCount==0 ? null : getChild(childCount-1);
}
/**
* Returns the number of lines that are hidden when this fold is
* collapsed.
*
* @return The number of lines hidden.
* @see #getStartLine()
* @see #getEndLine()
*/
public int getLineCount() {
return getEndLine() - getStartLine();
}
/**
* Returns the parent fold of this one.
*
* @return The parent fold, or <code>null</code> if this is a top-level
* fold.
*/
public Fold getParent() {
return parent;
}
/**
* Returns the starting line of this fold region. This is the only line
* in the fold region that is not hidden when a fold is collapsed.<p>
*
* The value returned by this method will automatically update as the
* text area's contents are modified, to track the starting line of the
* code block.
*
* @return The starting line of the code block.
* @see #getEndLine()
* @see #getStartOffset()
*/
public int getStartLine() {
int startOffs = getStartOffset();
if (lastStartOffs==startOffs) {
return cachedStartLine;
}
lastStartOffs = startOffs;
Element root = textArea.getDocument().getDefaultRootElement();
return cachedStartLine = root.getElementIndex(startOffs);
}
/**
* Returns the starting offset of this fold region. For example, for
* languages such as C and Java, this would be the offset of the opening
* curly brace of a code block.<p>
*
* The value returned by this method will automatically update as the
* text area's contents are modified, to track the starting offset of the
* code block.
*
* @return The start offset of this fold.
* @see #getStartLine()
* @see #getEndOffset()
*/
public int getStartOffset() {
return startOffs.getOffset();
}
@Override
public int hashCode() {
return getStartLine();
}
/**
* Returns whether this fold is collapsed.
*
* @return Whether this fold is collapsed.
* @see #setCollapsed(boolean)
* @see #toggleCollapsedState()
*/
public boolean isCollapsed() {
return collapsed;
}
/**
* Returns whether this fold is entirely on a single line. In general,
* a {@link FoldParser} should not remember fold regions all on a single
* line, since there's really nothing to fold.
*
* @return Whether this fold is on a single line.
* @see #removeFromParent()
*/
public boolean isOnSingleLine() {
return getStartLine()==getEndLine();
}
/**
* Removes this fold from its parent. This should only be called by
* {@link FoldParser} implementations if they determine that a fold is all
* on a single line (and thus shouldn't be remembered) after creating it.
*
* @return Whether this fold had a parent to be removed from.
* @see #isOnSingleLine()
*/
public boolean removeFromParent() {
if (parent!=null) {
parent.removeMostRecentChild();
parent = null;
return true;
}
return false;
}
private void removeMostRecentChild() {
children.remove(children.size()-1);
}
/**
* Sets whether this <code>Fold</code> is collapsed. Calling this method
* will update both the text area and all <code>Gutter</code> components.
*
* @param collapsed Whether this fold should be collapsed.
* @see #isCollapsed()
* @see #toggleCollapsedState()
*/
public void setCollapsed(boolean collapsed) {
if (collapsed!=this.collapsed) {
// Change our fold state and cached info about folded line count.
int lineCount = getLineCount();
int linesToCollapse = lineCount - childCollapsedLineCount;
if (!collapsed) { // If we're expanding
linesToCollapse = -linesToCollapse;
}
//System.out.println("Hiding lines: " + linesToCollapse +
// " (" + lineCount + ", " + linesToCollapse + ")");
this.collapsed = collapsed;
if (parent!=null) {
parent.updateChildCollapsedLineCount(linesToCollapse);
}
// If an end point of the selection is being hidden, move the caret
// "out" of the fold.
if (collapsed) {
int dot = textArea.getSelectionStart(); // Forgive variable name
Element root = textArea.getDocument().getDefaultRootElement();
int dotLine = root.getElementIndex(dot);
boolean updateCaret = containsLine(dotLine);
if (!updateCaret) {
int mark = textArea.getSelectionEnd();
if (mark!=dot) {
int markLine = root.getElementIndex(mark);
updateCaret = containsLine(markLine);
}
}
if (updateCaret) {
dot = root.getElement(getStartLine()).getEndOffset() - 1;
textArea.setCaretPosition(dot);
}
}
textArea.foldToggled(this);
}
}
/**
* Sets the ending offset of this fold, such as the closing curly brace
* of a code block in C or Java. {@link FoldParser} implementations should
* call this on an existing <code>Fold</code> upon finding its end. If
* this method isn't called, then this <code>Fold</code> is considered to
* have no end, i.e., it will collapse everything to the end of the file.
*
* @param endOffs The end offset of this fold.
* @throws BadLocationException If <code>endOffs</code> is not a valid
* location in the text area.
*/
public void setEndOffset(int endOffs) throws BadLocationException {
this.endOffs = textArea.getDocument().createPosition(endOffs);
}
/**
* Toggles the collapsed state of this fold.
*
* @see #setCollapsed(boolean)
*/
public void toggleCollapsedState() {
setCollapsed(!collapsed);
}
private void updateChildCollapsedLineCount(int count) {
childCollapsedLineCount += count;
//if (childCollapsedLineCount>getLineCount()) {
// Thread.dumpStack();
//}
if (!collapsed && parent!=null) {
parent.updateChildCollapsedLineCount(count);
}
}
/**
* Overridden for debugging purposes.
*
* @return A string representation of this <code>Fold</code>.
*/
@Override
public String toString() {
return "[Fold: " +
"startOffs=" + getStartOffset() +
", endOffs=" + getEndOffset() +
", collapsed=" + collapsed +
"]";
}
}