source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/folding/FoldManager.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 17.8 KB
Line 
1/*
2 * 10/08/2011
3 *
4 * FoldManager.java - Manages code folding in an RSyntaxTextArea instance.
5 *
6 * This library is distributed under a modified BSD license. See the included
7 * RSyntaxTextArea.License.txt file for details.
8 */
9package org.fife.ui.rsyntaxtextarea.folding;
10
11import java.beans.PropertyChangeEvent;
12import java.beans.PropertyChangeListener;
13import java.beans.PropertyChangeSupport;
14import java.util.ArrayList;
15import java.util.Collections;
16import java.util.List;
17import javax.swing.event.DocumentEvent;
18import javax.swing.event.DocumentListener;
19import javax.swing.text.BadLocationException;
20import javax.swing.text.Document;
21import javax.swing.text.Element;
22
23import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
24import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
25import org.fife.ui.rsyntaxtextarea.parser.AbstractParser;
26import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult;
27import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
28import org.fife.ui.rsyntaxtextarea.parser.Parser;
29
30
31/**
32 * Manages code folding in an instance of RSyntaxTextArea.
33 *
34 * @author Robert Futrell
35 * @version 1.0
36 */
37public class FoldManager {
38
39 private RSyntaxTextArea textArea;
40 private FoldParser parser;
41 private List folds;
42 private boolean codeFoldingEnabled;
43 private PropertyChangeSupport support;
44
45
46 /**
47 * Property fired when folds have been updated.
48 */
49 public static final String PROPERTY_FOLDS_UPDATED = "FoldsUpdated";
50
51
52 /**
53 * Constructor.
54 *
55 * @param textArea The text area whose folds we are managing.
56 */
57 public FoldManager(RSyntaxTextArea textArea) {
58 this.textArea = textArea;
59 support = new PropertyChangeSupport(this);
60 Listener l = new Listener();
61 textArea.getDocument().addDocumentListener(l);
62 textArea.addPropertyChangeListener(RSyntaxTextArea.SYNTAX_STYLE_PROPERTY, l);
63 folds = new ArrayList();
64 updateFoldParser();
65 }
66
67
68 /**
69 * Adds a property change listener to this fold manager.
70 *
71 * @param l The new listener.
72 * @see #removePropertyChangeListener(PropertyChangeListener)
73 */
74 public void addPropertyChangeListener(PropertyChangeListener l) {
75 support.addPropertyChangeListener(l);
76 }
77
78
79 /**
80 * Removes all folds.
81 */
82 public void clear() {
83 folds.clear();
84 }
85
86
87 /**
88 * Ensures that the specified offset is not hidden in a collapsed fold.
89 * Any folds containing this offset that are collapsed will be expanded.
90 *
91 * @param offs The offset.
92 * @return Whether any folds had to be opened.
93 * @see #getDeepestFoldContaining(int)
94 */
95 public boolean ensureOffsetNotInClosedFold(int offs) {
96 boolean foldsOpened = false;
97 Fold fold = getDeepestFoldContaining(offs);
98 while (fold!=null) {
99 if (fold.isCollapsed()) {
100 fold.setCollapsed(false);
101 foldsOpened = true;
102 }
103 fold = fold.getParent();
104 }
105 return foldsOpened;
106 }
107
108
109 /**
110 * Returns the "deepest" nested fold containing the specified offset.
111 *
112 * @param offs The offset.
113 * @return The deepest fold containing the offset, or <code>null</code> if
114 * no fold contains the offset.
115 */
116 public Fold getDeepestFoldContaining(int offs) {
117 Fold deepestFold = null;
118 if (offs>-1) {
119 for (int i=0; i<folds.size(); i++) {
120 Fold fold = getFold(i);
121 if (fold.containsOffset(offs)) {
122 deepestFold = fold.getDeepestFoldContaining(offs);
123 break;
124 }
125 }
126 }
127 return deepestFold;
128 }
129
130
131 /**
132 * Returns the "deepest" open fold containing the specified offset.
133 *
134 * @param offs The offset.
135 * @return The fold, or <code>null</code> if no open fold contains the
136 * offset.
137 */
138 public Fold getDeepestOpenFoldContaining(int offs) {
139
140 Fold deepestFold = null;
141
142 if (offs>-1) {
143 for (int i=0; i<folds.size(); i++) {
144 Fold fold = getFold(i);
145 if (fold.containsOffset(offs)) {
146 if (fold.isCollapsed()) {
147 return null;
148 }
149 deepestFold = fold.getDeepestOpenFoldContaining(offs);
150 break;
151 }
152 }
153 }
154
155 return deepestFold;
156
157 }
158
159
160 /**
161 * Returns a specific top-level fold, which may have child folds.
162 *
163 * @param index The index of the fold.
164 * @return The fold.
165 * @see #getFoldCount()
166 */
167 public Fold getFold(int index) {
168 return (Fold)folds.get(index);
169 }
170
171
172 /**
173 * Returns the number of top-level folds.
174 *
175 * @return The number of top-level folds.
176 * @see #getFold(int)
177 */
178 public int getFoldCount() {
179 return folds.size();
180 }
181
182
183 /**
184 * Returns the fold region that starts at the specified line.
185 *
186 * @param line The line number.
187 * @return The fold, or <code>null</code> if the line is not the start
188 * of a fold region.
189 * @see #isFoldStartLine(int)
190 */
191 public Fold getFoldForLine(int line) {
192 return getFoldForLineImpl(null, folds, line);
193 }
194
195
196private Fold getFoldForLineImpl(Fold parent, List folds, int line) {
197
198 int low = 0;
199 int high = folds.size() - 1;
200
201 while (low <= high) {
202 int mid = (low + high) >> 1;
203 Fold midFold = (Fold)folds.get(mid);
204 int startLine = midFold.getStartLine();
205 if (line==startLine) {
206 return midFold;
207 }
208 else if (line<startLine) {
209 high = mid - 1;
210 }
211 else {
212 int endLine = midFold.getEndLine();
213 if (line>=endLine) {
214 low = mid + 1;
215 }
216 else { // line>startLine && line<=endLine
217 List children = midFold.getChildren();
218 return children!=null ? getFoldForLineImpl(midFold, children, line) : null;
219 }
220 }
221 }
222
223 return null; // No fold for this line
224}
225
226
227 /**
228 * Returns the total number of hidden (folded) lines.
229 *
230 * @return The total number of hidden (folded) lines.
231 * @see #getHiddenLineCountAbove(int)
232 */
233 public int getHiddenLineCount() {
234 int count = 0;
235 for (int i=0; i<folds.size(); i++) {
236 count += ((Fold)folds.get(i)).getCollapsedLineCount();
237 }
238 return count;
239 }
240
241
242 /**
243 * Returns the number of lines "hidden" by collapsed folds above the
244 * specified line.
245 *
246 * @param line The line. This is the line number for a logical line.
247 * For the line number of a physical line (i.e. visible, not folded),
248 * use <code>getHiddenLineCountAbove(int, true)</code>.
249 * @return The number of lines hidden in folds above <code>line</code>.
250 * @see #getHiddenLineCountAbove(int, boolean)
251 */
252 public int getHiddenLineCountAbove(int line) {
253 return getHiddenLineCountAbove(line, false);
254 }
255
256
257 /**
258 * Returns the number of lines "hidden" by collapsed folds above the
259 * specified line.
260 *
261 * @param line The line.
262 * @param physical Whether <code>line</code> is the number of a physical
263 * line (i.e. visible, not code-folded), or a logical one (i.e. any
264 * line from the model). If <code>line</code> was determined by a
265 * raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>),
266 * this value should be <code>true</code>. It should be
267 * <code>false</code> when it was calculated from an offset in the
268 * document (for example).
269 * @return The number of lines hidden in folds above <code>line</code>.
270 */
271 public int getHiddenLineCountAbove(int line, boolean physical) {
272
273 int count = 0;
274
275 for (int i=0; i<folds.size(); i++) {
276 Fold fold = (Fold)folds.get(i);
277 int comp = physical ? line+count : line;
278 if (fold.getStartLine()>=comp) {
279 break;
280 }
281 count += getHiddenLineCountAboveImpl(fold, comp, physical);
282 }
283
284 return count;
285
286 }
287
288
289 /**
290 * Returns the number of lines "hidden" by collapsed folds above the
291 * specified line.
292 *
293 * @param fold The current fold in the recursive algorithm. It and its
294 * children are examined.
295 * @param line The line.
296 * @param physical Whether <code>line</code> is the number of a physical
297 * line (i.e. visible, not code-folded), or a logical one (i.e. any
298 * line from the model). If <code>line</code> was determined by a
299 * raw line calculation (i.e. <code>(visibleTopY / lineHeight)</code>),
300 * this value should be <code>true</code>. It should be
301 * <code>false</code> when it was calculated from an offset in the
302 * document (for example).
303 * @return The number of lines hidden in folds that are descendants of
304 * <code>fold</code>, or <code>fold</code> itself, above
305 * <code>line</code>.
306 */
307 private int getHiddenLineCountAboveImpl(Fold fold, int line, boolean physical) {
308
309 int count = 0;
310
311 if (fold.getEndLine()<line ||
312 (fold.isCollapsed() && fold.getStartLine()<line)) {
313 count = fold.getCollapsedLineCount();
314 }
315 else {
316 int childCount = fold.getChildCount();
317 for (int i=0; i<childCount; i++) {
318 Fold child = fold.getChild(i);
319 int comp = physical ? line+count : line;
320 if (child.getStartLine()>=comp) {
321 break;
322 }
323 count += getHiddenLineCountAboveImpl(child, comp, physical);
324 }
325 }
326
327 return count;
328
329 }
330
331
332 /**
333 * Returns the last visible line in the text area, taking into account
334 * folds.
335 *
336 * @return The last visible line.
337 */
338 public int getLastVisibleLine() {
339
340 int lastLine = textArea.getLineCount() - 1;
341
342 if (isCodeFoldingSupportedAndEnabled()) {
343 int foldCount = getFoldCount();
344 if (foldCount>0) {
345 Fold lastFold = getFold(foldCount-1);
346 if (lastFold.containsLine(lastLine)) {
347 if (lastFold.isCollapsed()) {
348 lastLine = lastFold.getStartLine();
349 }
350 else { // Child fold may end on the same line as parent
351 while (lastFold.getHasChildFolds()) {
352 lastFold = lastFold.getLastChild();
353 if (lastFold.containsLine(lastLine)) {
354 if (lastFold.isCollapsed()) {
355 lastLine = lastFold.getStartLine();
356 break;
357 }
358 }
359 else { // Higher up
360 break;
361 }
362 }
363 }
364 }
365 }
366 }
367
368 return lastLine;
369
370 }
371
372
373 public int getVisibleLineAbove(int line) {
374
375 if (line<=0 || line>=textArea.getLineCount()) {
376 return -1;
377 }
378
379 do {
380 line--;
381 } while (line>=0 && isLineHidden(line));
382
383 return line;
384
385 }
386
387
388 public int getVisibleLineBelow(int line) {
389
390 int lineCount = textArea.getLineCount();
391 if (line<0 || line>=lineCount-1) {
392 return -1;
393 }
394
395 do {
396 line++;
397 } while (line<lineCount && isLineHidden(line));
398
399 return line==lineCount ? -1 : line;
400
401 }
402
403
404// private static int binaryFindFoldContainingLine(int line) {
405//
406//List allFolds;
407//
408// int low = 0;
409// int high = allFolds.size() - 1;
410//
411// while (low <= high) {
412// int mid = (low + high) >> 1;
413// Fold midVal = (Fold)allFolds.get(mid);
414// if (midVal.containsLine(line)) {
415// return mid;
416// }
417// if (line<=midVal.getStartLine()) {
418// high = mid - 1;
419// }
420// else { // line > midVal.getEndLine()
421// low = mid + 1;
422// }
423// }
424//
425// return -(low + 1); // key not found
426//
427// }
428
429
430 /**
431 * Returns whether code folding is enabled. Note that only certain
432 * languages support code folding; those that do not will ignore this
433 * property.
434 *
435 * @return Whether code folding is enabled.
436 * @see #setCodeFoldingEnabled(boolean)
437 */
438 public boolean isCodeFoldingEnabled() {
439 return codeFoldingEnabled;
440 }
441
442
443 /**
444 * Returns <code>true</code> if and only if code folding is enabled for
445 * this text area, AND folding is supported for the language it is editing.
446 * Whether or not folding is supported for a language depends on whether
447 * a fold parser is registered for that language with the
448 * <code>FoldParserManager</code>.
449 *
450 * @return Whether folding is supported and enabled for this text area.
451 * @see FoldParserManager
452 */
453 public boolean isCodeFoldingSupportedAndEnabled() {
454 return codeFoldingEnabled && parser!=null;
455 }
456
457
458 /**
459 * Returns whether the specified line contains the start of a fold region.
460 *
461 * @param line The line.
462 * @return Whether the line contains the start of a fold region.
463 * @see #getFoldForLine(int)
464 */
465 public boolean isFoldStartLine(int line) {
466 return getFoldForLine(line)!=null;
467 }
468
469
470 /**
471 * Returns whether a line is hidden in a collapsed fold.
472 *
473 * @param line The line to check.
474 * @return Whether the line is hidden in a collapsed fold.
475 */
476 public boolean isLineHidden(int line) {
477 for (int i=0; i<folds.size(); i++) {
478 Fold fold = (Fold)folds.get(i);
479 if (fold.containsLine(line)) {
480 if (fold.isCollapsed()) {
481 return true;
482 }
483 else {
484 return isLineHiddenImpl(fold, line);
485 }
486 }
487 }
488 return false;
489 }
490
491
492 private boolean isLineHiddenImpl(Fold parent, int line) {
493 for (int i=0; i<parent.getChildCount(); i++) {
494 Fold child = parent.getChild(i);
495 if (child.containsLine(line)) {
496 if (child.isCollapsed()) {
497 return true;
498 }
499 else {
500 return isLineHiddenImpl(child, line);
501 }
502 }
503 }
504 return false;
505 }
506
507
508 private void keepFoldState(Fold newFold, List oldFolds) {
509 int previousLoc = Collections.binarySearch(oldFolds, newFold);
510 //System.out.println(newFold + " => " + previousLoc);
511 if (previousLoc>=0) {
512 Fold prevFold = (Fold)oldFolds.get(previousLoc);
513 newFold.setCollapsed(prevFold.isCollapsed());
514 }
515 else {
516 //previousLoc = -(insertion point) - 1;
517 int insertionPoint = -(previousLoc + 1);
518 if (insertionPoint>0) {
519 Fold possibleParentFold = (Fold)oldFolds.get(insertionPoint-1);
520 List children = possibleParentFold.getChildren();
521 if (children!=null) {
522 keepFoldState(newFold, children);
523 }
524 }
525 }
526 }
527
528
529 /**
530 * Removes a property change listener from this fold manager.
531 *
532 * @param l The listener to remove.
533 * @see #addPropertyChangeListener(PropertyChangeListener)
534 */
535 public void removePropertyChangeListener(PropertyChangeListener l) {
536 support.removePropertyChangeListener(l);
537 }
538
539
540 /**
541 * Forces an immediate reparsing for folds, if folding is enabled. This
542 * usually does not need to be called by the programmer, since fold
543 * parsing is done automatically by RSTA.
544 */
545 public void reparse() {
546
547 if (codeFoldingEnabled && parser!=null) {
548
549 // Re-calculate folds. Keep the fold state of folds that are
550 // still around.
551 List newFolds = parser.getFolds(textArea);
552 if (newFolds==null) {
553 newFolds = Collections.EMPTY_LIST;
554 }
555 else {
556 for (int i=0; i<newFolds.size(); i++) {
557 Fold newFold = (Fold)newFolds.get(i);
558 keepFoldState(newFold, folds);
559 for (int j=0; j<newFold.getChildCount(); j++) {
560 keepFoldState(newFold.getChild(j), folds);
561 }
562 }
563 }
564 folds = newFolds;
565
566 // Let folks (gutter, etc.) know that folds have been updated.
567 support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, folds);
568 textArea.repaint();
569
570 }
571 else {
572 folds.clear();
573 }
574
575 }
576
577 /**
578 * Sets whether code folding is enabled. Note that only certain
579 * languages will support code folding out of the box. Those languages
580 * which do not support folding will ignore this property.
581 *
582 * @param enabled Whether code folding should be enabled.
583 * @see #isCodeFoldingEnabled()
584 */
585 public void setCodeFoldingEnabled(boolean enabled) {
586 if (enabled!=codeFoldingEnabled) {
587 codeFoldingEnabled = enabled;
588 if (tempParser!=null) {
589 textArea.removeParser(tempParser);
590 }
591 if (enabled) {
592 tempParser = new AbstractParser() {
593 public ParseResult parse(RSyntaxDocument doc, String style) {
594 reparse();
595 return new DefaultParseResult(this);
596 }
597 };
598 textArea.addParser(tempParser);
599 support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null);
600 //reparse();
601 }
602 else {
603 folds = Collections.EMPTY_LIST;
604 textArea.repaint();
605 support.firePropertyChange(PROPERTY_FOLDS_UPDATED, null, null);
606 }
607 }
608 }
609private Parser tempParser;
610
611
612 /**
613 * Sets the folds for this fold manager.
614 *
615 * @param folds The new folds. This should not be <code>null</code>.
616 */
617 public void setFolds(List folds) {
618 this.folds = folds;
619 }
620
621
622 /**
623 * Updates the fold parser to be the one appropriate for the language
624 * currently being highlighted.
625 */
626 private void updateFoldParser() {
627 parser = FoldParserManager.get().getFoldParser(
628 textArea.getSyntaxEditingStyle());
629 }
630
631
632 /**
633 * Listens for events in the text editor.
634 */
635 private class Listener implements DocumentListener, PropertyChangeListener {
636
637 public void changedUpdate(DocumentEvent e) {
638 }
639
640 public void insertUpdate(DocumentEvent e) {
641 // Adding text containing a newline to the visible line of a folded
642 // Fold causes that Fold to unfold. Check only start offset of
643 // insertion since that's the line that was "modified".
644 int startOffs = e.getOffset();
645 int endOffs = startOffs + e.getLength();
646 Document doc = e.getDocument();
647 Element root = doc.getDefaultRootElement();
648 int startLine = root.getElementIndex(startOffs);
649 int endLine = root.getElementIndex(endOffs);
650 if (startLine!=endLine) { // Inserted text covering > 1 line...
651 Fold fold = getFoldForLine(startLine);
652 if (fold!=null && fold.isCollapsed()) {
653 fold.toggleCollapsedState();
654 }
655 }
656 }
657
658 public void propertyChange(PropertyChangeEvent e) {
659 // Syntax style changed in editor.
660 updateFoldParser();
661 reparse(); // Even if no fold parser change, highlighting did
662 }
663 public void removeUpdate(DocumentEvent e) {
664 // Removing text from the visible line of a folded Fold causes that
665 // Fold to unfold. We only need to check the removal offset since
666 // that's the new caret position.
667 int offs = e.getOffset();
668 try {
669 int lastLineModified = textArea.getLineOfOffset(offs);
670 //System.out.println(">>> " + lastLineModified);
671 Fold fold = getFoldForLine(lastLineModified);
672 //System.out.println("&&& " + fold);
673 if (fold!=null && fold.isCollapsed()) {
674 fold.toggleCollapsedState();
675 }
676 } catch (BadLocationException ble) {
677 ble.printStackTrace(); // Never happens
678 }
679 }
680
681 }
682
683
684}
Note: See TracBrowser for help on using the repository browser.