/* * 10/08/2011 * * FoldManager.java - Manages code folding 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.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.parser.AbstractParser; import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult; import org.fife.ui.rsyntaxtextarea.parser.ParseResult; import org.fife.ui.rsyntaxtextarea.parser.Parser; import org.fife.ui.rtextarea.RDocument; /** * Manages code folding in an instance of RSyntaxTextArea. * * @author Robert Futrell * @version 1.0 */ public class FoldManager { private RSyntaxTextArea textArea; private FoldParser parser; private List<Fold> folds; private boolean codeFoldingEnabled; private PropertyChangeSupport support; private Listener l; /** * Property fired when folds have been updated. */ public static final String PROPERTY_FOLDS_UPDATED = "FoldsUpdated"; /** * Constructor. * * @param textArea The text area whose folds we are managing. */ public FoldManager(RSyntaxTextArea textArea) { this.textArea = textArea; support = new PropertyChangeSupport(this); l = new Listener(); textArea.getDocument().addDocumentListener(l); textArea.addPropertyChangeListener(RSyntaxTextArea.SYNTAX_STYLE_PROPERTY, l); textArea.addPropertyChangeListener("document", l); folds = new ArrayList<Fold>(); updateFoldParser(); } /** * Adds a property change listener to this fold manager. * * @param l The new listener. * @see #removePropertyChangeListener(PropertyChangeListener) */ public void addPropertyChangeListener(PropertyChangeListener l) { support.addPropertyChangeListener(l); } /** * Removes all folds. */ public void clear() { folds.clear(); } /** * Ensures that the specified offset is not hidden in a collapsed fold. * Any folds containing this offset that are collapsed will be expanded. * * @param offs The offset. * @return Whether any folds had to be opened. * @see #getDeepestFoldContaining(int) */ public boolean ensureOffsetNotInClosedFold(int offs) { boolean foldsOpened = false; Fold fold = getDeepestFoldContaining(offs); while (fold!=null) { if (fold.isCollapsed()) { fold.setCollapsed(false); foldsOpened = true; } fold = fold.getParent(); } return foldsOpened; } /** * Returns the "deepest" nested fold containing the specified offset. * * @param offs The offset. * @return The deepest fold containing the offset, or <code>null</code> if * no fold contains the offset. */ public Fold getDeepestFoldContaining(int offs) { Fold deepestFold = null; if (offs>-1) { for (int i=0; i<folds.size(); i++) { Fold fold = getFold(i); if (fold.containsOffset(offs)) { deepestFold = fold.getDeepestFoldContaining(offs); break; } } } return deepestFold; } /** * Returns the "deepest" open fold containing the specified offset. * * @param offs The offset. * @return The fold, or <code>null</code> if no open fold contains the * offset. */ public Fold getDeepestOpenFoldContaining(int offs) { Fold deepestFold = null; if (offs>-1) { for (int i=0; i<folds.size(); i++) { Fold fold = getFold(i); if (fold.containsOffset(offs)) { if (fold.isCollapsed()) { return null; } deepestFold = fold.getDeepestOpenFoldContaining(offs); break; } } } return deepestFold; } /** * Returns a specific top-level fold, which may have child folds. * * @param index The index of the fold. * @return The fold. * @see #getFoldCount() */ public Fold getFold(int index) { return folds.get(index); } /** * Returns the number of top-level folds. * * @return The number of top-level folds. * @see #getFold(int) */ public int getFoldCount() { return folds.size(); } /** * Returns the fold region that starts at the specified line. * * @param line The line number. * @return The fold, or <code>null</code> if the line is not the start * of a fold region. * @see #isFoldStartLine(int) */ public Fold getFoldForLine(int line) { return getFoldForLineImpl(null, folds, line); } private Fold getFoldForLineImpl(Fold parent, List<Fold> folds, int line) { int low = 0; int high = folds.size() - 1; while (low <= high) { int mid = (low + high) >> 1; Fold midFold = folds.get(mid); int startLine = midFold.getStartLine(); if (line==startLine) { return midFold; } else if (line<startLine) { high = mid - 1; } else { int endLine = midFold.getEndLine(); if (line>=endLine) { low = mid + 1; } else { // line>startLine && line<=endLine List<Fold> children = midFold.getChildren(); return children!=null ? getFoldForLineImpl(midFold, children, line) : null; } } } return null; // No fold for this line } /** * Returns the total number of hidden (folded) lines. * * @return The total number of hidden (folded) lines. * @see #getHiddenLineCountAbove(int) */ public int getHiddenLineCount() { int count = 0; for (Fold fold : folds) { count += fold.getCollapsedLineCount(); } return count; } /** * Returns the number of lines "hidden" by collapsed folds above the * specified line. * * @param line The line. This is the line number for a logical line. * For the line number of a physical line (i.e. visible, not folded), * use <code>getHiddenLineCountAbove(int, true)</code>. * @return The number of lines hidden in folds above <code>line</code>. * @see #getHiddenLineCountAbove(int, boolean) */ public int getHiddenLineCountAbove(int line) { return getHiddenLineCountAbove(line, false); } /** * Returns the number of lines "hidden" by collapsed folds above the * specified line. * * @param line The line. * @param physical Whether <code>line</code> is the number of a physical * line (i.e. visible, not code-folded), or a logical one (i.e. any * line from the model). If <code>line</code> was determined by a * raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>), * this value should be <code>true</code>. It should be * <code>false</code> when it was calculated from an offset in the * document (for example). * @return The number of lines hidden in folds above <code>line</code>. */ public int getHiddenLineCountAbove(int line, boolean physical) { int count = 0; for (Fold fold : folds) { int comp = physical ? line+count : line; if (fold.getStartLine()>=comp) { break; } count += getHiddenLineCountAboveImpl(fold, comp, physical); } return count; } /** * Returns the number of lines "hidden" by collapsed folds above the * specified line. * * @param fold The current fold in the recursive algorithm. It and its * children are examined. * @param line The line. * @param physical Whether <code>line</code> is the number of a physical * line (i.e. visible, not code-folded), or a logical one (i.e. any * line from the model). If <code>line</code> was determined by a * raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>), * this value should be <code>true</code>. It should be * <code>false</code> when it was calculated from an offset in the * document (for example). * @return The number of lines hidden in folds that are descendants of * <code>fold</code>, or <code>fold</code> itself, above * <code>line</code>. */ private int getHiddenLineCountAboveImpl(Fold fold, int line, boolean physical) { int count = 0; if (fold.getEndLine()<line || (fold.isCollapsed() && fold.getStartLine()<line)) { count = fold.getCollapsedLineCount(); } else { int childCount = fold.getChildCount(); for (int i=0; i<childCount; i++) { Fold child = fold.getChild(i); int comp = physical ? line+count : line; if (child.getStartLine()>=comp) { break; } count += getHiddenLineCountAboveImpl(child, comp, physical); } } return count; } /** * Returns the last visible line in the text area, taking into account * folds. * * @return The last visible line. */ public int getLastVisibleLine() { int lastLine = textArea.getLineCount() - 1; if (isCodeFoldingSupportedAndEnabled()) { int foldCount = getFoldCount(); if (foldCount>0) { Fold lastFold = getFold(foldCount-1); if (lastFold.containsLine(lastLine)) { if (lastFold.isCollapsed()) { lastLine = lastFold.getStartLine(); } else { // Child fold may end on the same line as parent while (lastFold.getHasChildFolds()) { lastFold = lastFold.getLastChild(); if (lastFold.containsLine(lastLine)) { if (lastFold.isCollapsed()) { lastLine = lastFold.getStartLine(); break; } } else { // Higher up break; } } } } } } return lastLine; } public int getVisibleLineAbove(int line) { if (line<=0 || line>=textArea.getLineCount()) { return -1; } do { line--; } while (line>=0 && isLineHidden(line)); return line; } public int getVisibleLineBelow(int line) { int lineCount = textArea.getLineCount(); if (line<0 || line>=lineCount-1) { return -1; } do { line++; } while (line<lineCount && isLineHidden(line)); return line==lineCount ? -1 : line; } // private static int binaryFindFoldContainingLine(int line) { // //List allFolds; // // int low = 0; // int high = allFolds.size() - 1; // // while (low <= high) { // int mid = (low + high) >> 1; // Fold midVal = (Fold)allFolds.get(mid); // if (midVal.containsLine(line)) { // return mid; // } // if (line<=midVal.getStartLine()) { // high = mid - 1; // } // else { // line > midVal.getEndLine() // low = mid + 1; // } // } // // return -(low + 1); // key not found // // } /** * Returns whether code folding is enabled. Note that only certain * languages support code folding; those that do not will ignore this * property. * * @return Whether code folding is enabled. * @see #setCodeFoldingEnabled(boolean) */ public boolean isCodeFoldingEnabled() { return codeFoldingEnabled; } /** * Returns <code>true</code> if and only if code folding is enabled for * this text area, AND folding is supported for the language it is editing. * Whether or not folding is supported for a language depends on whether * a fold parser is registered for that language with the * <code>FoldParserManager</code>. * * @return Whether folding is supported and enabled for this text area. * @see FoldParserManager */ public boolean isCodeFoldingSupportedAndEnabled() { return codeFoldingEnabled && parser!=null; } /** * Returns whether the specified line contains the start of a fold region. * * @param line The line. * @return Whether the line contains the start of a fold region. * @see #getFoldForLine(int) */ public boolean isFoldStartLine(int line) { return getFoldForLine(line)!=null; } /** * Returns whether a line is hidden in a collapsed fold. * * @param line The line to check. * @return Whether the line is hidden in a collapsed fold. */ public boolean isLineHidden(int line) { for (Fold fold : folds) { if (fold.containsLine(line)) { if (fold.isCollapsed()) { return true; } else { return isLineHiddenImpl(fold, line); } } } return false; } private boolean isLineHiddenImpl(Fold parent, int line) { for (int i=0; i<parent.getChildCount(); i++) { Fold child = parent.getChild(i); if (child.containsLine(line)) { if (child.isCollapsed()) { return true; } else { return isLineHiddenImpl(child, line); } } } return false; } /** * Checks whether a single fold was there in the "old" set of folds. If * it was, its collapsed state is preserved. * * @param newFold The "new" fold to check for. * @param oldFolds The previous folds before an edit occurred. */ private void keepFoldState(Fold newFold, List<Fold> oldFolds) { int previousLoc = Collections.binarySearch(oldFolds, newFold); //System.out.println(newFold + " => " + previousLoc); if (previousLoc>=0) { Fold prevFold = oldFolds.get(previousLoc); newFold.setCollapsed(prevFold.isCollapsed()); } else { //previousLoc = -(insertion point) - 1; int insertionPoint = -(previousLoc + 1); if (insertionPoint>0) { Fold possibleParentFold = oldFolds.get(insertionPoint-1); if (possibleParentFold.containsOffset( newFold.getStartOffset())) { List<Fold> children = possibleParentFold.getChildren(); if (children!=null) { keepFoldState(newFold, children); } } } } } /** * Called when new folds come in from the fold parser. Checks whether any * folds from the "old" fold list are still in the "new" list; if so, their * collapsed state is preserved. * * @param newFolds The "new" folds after an edit occurred. This cannot be * <code>null</code>. * @param oldFolds The previous folds before the edit occurred. */ private void keepFoldStates(List<Fold> newFolds, List<Fold> oldFolds) { for (Fold newFold : newFolds) { keepFoldState(newFold, folds); List<Fold> newChildFolds = newFold.getChildren(); if (newChildFolds!=null) { keepFoldStates(newChildFolds, oldFolds); } } } /** * Removes a property change listener from this fold manager. * * @param l The listener to remove. * @see #addPropertyChangeListener(PropertyChangeListener) */ public void removePropertyChangeListener(PropertyChangeListener l) { support.removePropertyChangeListener(l); } /** * Forces an immediate reparsing for folds, if folding is enabled. This * usually does not need to be called by the programmer, since fold * parsing is done automatically by RSTA. */ public void reparse() { if (codeFoldingEnabled && parser!=null) { // Re-calculate folds. Keep the fold state of folds that are // still around. List<Fold> newFolds = parser.getFolds(textArea); if (newFolds==null) { newFolds = Collections.emptyList(); } else { keepFoldStates(newFolds, folds); } folds = newFolds; // Let folks (gutter, etc.) know that folds have been updated. support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, folds); textArea.repaint(); } else { folds.clear(); } } /** * Sets whether code folding is enabled. Note that only certain * languages will support code folding out of the box. Those languages * which do not support folding will ignore this property. * * @param enabled Whether code folding should be enabled. * @see #isCodeFoldingEnabled() */ public void setCodeFoldingEnabled(boolean enabled) { if (enabled!=codeFoldingEnabled) { codeFoldingEnabled = enabled; if (tempParser!=null) { textArea.removeParser(tempParser); } if (enabled) { tempParser = new AbstractParser() { public ParseResult parse(RSyntaxDocument doc, String style) { reparse(); return new DefaultParseResult(this); } }; textArea.addParser(tempParser); support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null); //reparse(); } else { folds = Collections.emptyList(); textArea.repaint(); support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null); } } } private Parser tempParser; /** * Sets the folds for this fold manager. * * @param folds The new folds. This should not be <code>null</code>. */ public void setFolds(List<Fold> folds) { this.folds = folds; } /** * Updates the fold parser to be the one appropriate for the language * currently being highlighted. */ private void updateFoldParser() { parser = FoldParserManager.get().getFoldParser( textArea.getSyntaxEditingStyle()); } /** * Listens for events in the text editor. */ private class Listener implements DocumentListener, PropertyChangeListener { public void changedUpdate(DocumentEvent e) { } public void insertUpdate(DocumentEvent e) { // Adding text containing a newline to the visible line of a folded // Fold causes that Fold to unfold. Check only start offset of // insertion since that's the line that was "modified". int startOffs = e.getOffset(); int endOffs = startOffs + e.getLength(); Document doc = e.getDocument(); Element root = doc.getDefaultRootElement(); int startLine = root.getElementIndex(startOffs); int endLine = root.getElementIndex(endOffs); if (startLine!=endLine) { // Inserted text covering > 1 line... Fold fold = getFoldForLine(startLine); if (fold!=null && fold.isCollapsed()) { fold.toggleCollapsedState(); } } } public void propertyChange(PropertyChangeEvent e) { String name = e.getPropertyName(); if (RSyntaxTextArea.SYNTAX_STYLE_PROPERTY.equals(name)) { // Syntax style changed in editor. updateFoldParser(); reparse(); // Even if no fold parser change, highlighting did } else if ("document".equals(name)) { // The document switched out from under us RDocument old = (RDocument)e.getOldValue(); if (old != null) { old.removeDocumentListener(this); } RDocument newDoc = (RDocument)e.getNewValue(); if (newDoc != null) { newDoc.addDocumentListener(this); } reparse(); } } public void removeUpdate(DocumentEvent e) { // Removing text from the visible line of a folded Fold causes that // Fold to unfold. We only need to check the removal offset since // that's the new caret position. int offs = e.getOffset(); try { int lastLineModified = textArea.getLineOfOffset(offs); //System.out.println(">>> " + lastLineModified); Fold fold = getFoldForLine(lastLineModified); //System.out.println("&&& " + fold); if (fold!=null && fold.isCollapsed()) { fold.toggleCollapsedState(); } } catch (BadLocationException ble) { ble.printStackTrace(); // Never happens } } } }