Source code: org/gjt/sp/jedit/textarea/FoldVisibilityManager.java
1 /*
2 * FoldVisibilityManager.java - Controls fold visiblity
3 * :tabSize=8:indentSize=8:noTabs=false:
4 * :folding=explicit:collapseFolds=1:
5 *
6 * Copyright (C) 2001, 2002 Slava Pestov
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 */
22
23 package org.gjt.sp.jedit.textarea;
24
25 //{{{ Imports
26 import java.awt.Toolkit;
27 import org.gjt.sp.jedit.buffer.OffsetManager;
28 import org.gjt.sp.jedit.*;
29 //}}}
30
31 /**
32 * Manages fold visibility.<p>
33 *
34 * This class contains methods for translating between physical and virtual
35 * line numbers, for determining which lines are visible and which aren't,
36 * and for expanding and collapsing folds.<p>
37 *
38 * When jEdit's folding or narrowing features are used to hide
39 * portions of a buffer, the "virtual" line count visible
40 * in the text area is generally not equal to the "physical"
41 * line count of the buffer represented by the gutter's display.<p>
42 *
43 * You can use the {@link #physicalToVirtual(int)} and
44 * {@link #virtualToPhysical(int)} methods to convert one type of line
45 * number to another.
46 *
47 * @author Slava Pestov
48 * @author John Gellene (API documentation)
49 * @version $Id: FoldVisibilityManager.java,v 1.30 2003/02/21 20:12:19 spestov Exp $
50 * @since jEdit 4.0pre1
51 */
52 public class FoldVisibilityManager
53 {
54 //{{{ FoldVisibilityManager constructor
55 public FoldVisibilityManager(Buffer buffer, OffsetManager offsetMgr,
56 JEditTextArea textArea)
57 {
58 this.buffer = buffer;
59 this.offsetMgr = offsetMgr;
60 this.textArea = textArea;
61 } //}}}
62
63 //{{{ isNarrowed() method
64 /**
65 * Returns if the buffer has been narrowed.
66 * @since jEdit 4.0pre2
67 */
68 public boolean isNarrowed()
69 {
70 return narrowed;
71 } //}}}
72
73 //{{{ getVirtualLineCount() method
74 /**
75 * Returns the number of virtual lines in the buffer.
76 * @since jEdit 4.0pre1
77 */
78 public int getVirtualLineCount()
79 {
80 return offsetMgr.getVirtualLineCount(index);
81 } //}}}
82
83 //{{{ isLineVisible() method
84 /**
85 * Returns if the specified line is visible.
86 * @param line A physical line index
87 * @since jEdit 4.0pre1
88 */
89 public final boolean isLineVisible(int line)
90 {
91 if(line < 0 || line >= offsetMgr.getLineCount())
92 throw new ArrayIndexOutOfBoundsException(line);
93
94 try
95 {
96 buffer.readLock();
97 return offsetMgr.isLineVisible(line,index);
98 }
99 finally
100 {
101 buffer.readUnlock();
102 }
103 } //}}}
104
105 //{{{ getFirstVisibleLine() method
106 /**
107 * Returns the physical line number of the first visible line.
108 * @since jEdit 4.0pre3
109 */
110 public int getFirstVisibleLine()
111 {
112 try
113 {
114 buffer.readLock();
115
116 for(int i = 0; i < buffer.getLineCount(); i++)
117 {
118 if(offsetMgr.isLineVisible(i,index))
119 return i;
120 }
121 }
122 finally
123 {
124 buffer.readUnlock();
125 }
126
127 // can't happen?
128 return -1;
129 } //}}}
130
131 //{{{ getLastVisibleLine() method
132 /**
133 * Returns the physical line number of the last visible line.
134 * @since jEdit 4.0pre3
135 */
136 public int getLastVisibleLine()
137 {
138 try
139 {
140 buffer.readLock();
141
142 for(int i = buffer.getLineCount() - 1; i >= 0; i--)
143 {
144 if(offsetMgr.isLineVisible(i,index))
145 return i;
146 }
147 }
148 finally
149 {
150 buffer.readUnlock();
151 }
152
153 // can't happen?
154 return -1;
155 } //}}}
156
157 //{{{ getNextVisibleLine() method
158 /**
159 * Returns the next visible line after the specified line index.
160 * @param line A physical line index
161 * @since jEdit 4.0pre1
162 */
163 public int getNextVisibleLine(int line)
164 {
165 if(line < 0 || line >= offsetMgr.getLineCount())
166 throw new ArrayIndexOutOfBoundsException(line);
167
168 try
169 {
170 buffer.readLock();
171
172 if(line == buffer.getLineCount() - 1)
173 return -1;
174
175 for(int i = line + 1; i < buffer.getLineCount(); i++)
176 {
177 if(offsetMgr.isLineVisible(i,index))
178 return i;
179 }
180 return -1;
181 }
182 finally
183 {
184 buffer.readUnlock();
185 }
186 } //}}}
187
188 //{{{ getPrevVisibleLine() method
189 /**
190 * Returns the previous visible line before the specified line index.
191 * @param line A physical line index
192 * @since jEdit 4.0pre1
193 */
194 public int getPrevVisibleLine(int line)
195 {
196 if(line < 0 || line >= offsetMgr.getLineCount())
197 throw new ArrayIndexOutOfBoundsException(line);
198
199 try
200 {
201 buffer.readLock();
202
203 if(line == 0)
204 return -1;
205
206 for(int i = line - 1; i >= 0; i--)
207 {
208 if(offsetMgr.isLineVisible(i,index))
209 return i;
210 }
211 return -1;
212 }
213 finally
214 {
215 buffer.readUnlock();
216 }
217 } //}}}
218
219 //{{{ physicalToVirtual() method
220 /**
221 * Converts a physical line number to a virtual line number.
222 * @param line A physical line index
223 * @since jEdit 4.0pre1
224 */
225 public int physicalToVirtual(int line)
226 {
227 try
228 {
229 buffer.readLock();
230
231 if(line < 0)
232 throw new ArrayIndexOutOfBoundsException(line + " < 0");
233 else if(line >= offsetMgr.getLineCount())
234 {
235 throw new ArrayIndexOutOfBoundsException(line + " > "
236 + buffer.getLineCount());
237 }
238
239 // optimization
240 if(getVirtualLineCount() == buffer.getLineCount())
241 return line;
242
243 while(!offsetMgr.isLineVisible(line,index) && line > 0)
244 line--;
245
246 if(line == 0 && !offsetMgr.isLineVisible(line,index))
247 {
248 // inside the top narrow.
249 return 0;
250 }
251
252 if(lastPhysical == line)
253 {
254 if(lastVirtual < 0 || lastVirtual >= offsetMgr.getVirtualLineCount(index))
255 {
256 throw new ArrayIndexOutOfBoundsException(
257 "cached: " + lastVirtual);
258 }
259 }
260 else if(line > lastPhysical && lastPhysical != -1)
261 {
262 for(;;)
263 {
264 if(lastPhysical == line)
265 break;
266
267 if(offsetMgr.isLineVisible(lastPhysical,index))
268 lastVirtual++;
269
270 if(lastPhysical == buffer.getLineCount() - 1)
271 break;
272 else
273 lastPhysical++;
274 }
275
276 if(lastVirtual < 0 || lastVirtual >= offsetMgr.getVirtualLineCount(index))
277 {
278 throw new ArrayIndexOutOfBoundsException(
279 "fwd scan: " + lastVirtual);
280 }
281 }
282 else if(line < lastPhysical && lastPhysical - line > line)
283 {
284 for(;;)
285 {
286 if(lastPhysical == line)
287 break;
288
289 if(offsetMgr.isLineVisible(lastPhysical,index))
290 lastVirtual--;
291
292 if(lastPhysical == 0)
293 break;
294 else
295 lastPhysical--;
296 }
297
298 if(lastVirtual < 0 || lastVirtual >= offsetMgr.getVirtualLineCount(index))
299 {
300 throw new ArrayIndexOutOfBoundsException(
301 "back scan: " + lastVirtual);
302 }
303 }
304 else
305 {
306 lastPhysical = 0;
307 // find first visible line
308 while(!offsetMgr.isLineVisible(lastPhysical,index))
309 lastPhysical++;
310
311 lastVirtual = 0;
312 for(;;)
313 {
314 if(lastPhysical == line)
315 break;
316
317 if(offsetMgr.isLineVisible(lastPhysical,index))
318 lastVirtual++;
319
320 if(lastPhysical == buffer.getLineCount() - 1)
321 break;
322 else
323 lastPhysical++;
324 }
325
326 if(lastVirtual < 0 || lastVirtual >= offsetMgr.getVirtualLineCount(index))
327 {
328 throw new ArrayIndexOutOfBoundsException(
329 "zero scan: " + lastVirtual);
330 }
331 }
332
333 return lastVirtual;
334 }
335 finally
336 {
337 buffer.readUnlock();
338 }
339 } //}}}
340
341 //{{{ virtualToPhysical() method
342 /**
343 * Converts a virtual line number to a physical line number.
344 * @param line A virtual line index
345 * @since jEdit 4.0pre1
346 */
347 public int virtualToPhysical(int line)
348 {
349 try
350 {
351 buffer.readLock();
352
353 if(line < 0)
354 throw new ArrayIndexOutOfBoundsException(line + " < 0");
355 else if(line >= offsetMgr.getVirtualLineCount(index))
356 {
357 throw new ArrayIndexOutOfBoundsException(line + " > "
358 + offsetMgr.getVirtualLineCount(index));
359 }
360
361 // optimization
362 if(getVirtualLineCount() == buffer.getLineCount())
363 return line;
364
365 if(lastVirtual == line)
366 {
367 if(lastPhysical < 0 || lastPhysical >= buffer.getLineCount())
368 {
369 throw new ArrayIndexOutOfBoundsException(
370 "cached: " + lastPhysical);
371 }
372 }
373 else if(line > lastVirtual && lastVirtual != -1)
374 {
375 for(;;)
376 {
377 if(offsetMgr.isLineVisible(lastPhysical,index))
378 {
379 if(lastVirtual == line)
380 break;
381 else
382 lastVirtual++;
383 }
384
385 if(lastPhysical == buffer.getLineCount() - 1)
386 break;
387 else
388 lastPhysical++;
389 }
390
391 if(lastPhysical < 0 || lastPhysical >= buffer.getLineCount())
392 {
393 throw new ArrayIndexOutOfBoundsException(
394 "fwd scan: " + lastPhysical);
395 }
396 }
397 else if(line < lastVirtual && lastVirtual - line > line)
398 {
399 for(;;)
400 {
401 if(offsetMgr.isLineVisible(lastPhysical,index))
402 {
403 if(lastVirtual == line)
404 break;
405 else
406 lastVirtual--;
407 }
408
409 if(lastPhysical == 0)
410 break;
411 else
412 lastPhysical--;
413 }
414
415 if(lastPhysical < 0 || lastPhysical >= buffer.getLineCount())
416 {
417 throw new ArrayIndexOutOfBoundsException(
418 "back scan: " + lastPhysical);
419 }
420 }
421 else
422 {
423
424 lastPhysical = 0;
425 // find first visible line
426 while(!offsetMgr.isLineVisible(lastPhysical,index))
427 lastPhysical++;
428
429 lastVirtual = 0;
430 for(;;)
431 {
432 if(offsetMgr.isLineVisible(lastPhysical,index))
433 {
434 if(lastVirtual == line)
435 break;
436 else
437 lastVirtual++;
438 }
439
440 if(lastPhysical == buffer.getLineCount() - 1)
441 break;
442 else
443 lastPhysical++;
444 }
445
446 if(lastPhysical < 0 || lastPhysical >= buffer.getLineCount())
447 {
448 throw new ArrayIndexOutOfBoundsException(
449 "zero scan: " + lastPhysical);
450 }
451 }
452
453 return lastPhysical;
454 }
455 finally
456 {
457 buffer.readUnlock();
458 }
459 } //}}}
460
461 //{{{ collapseFold() method
462 /**
463 * Collapses the fold at the specified physical line index.
464 * @param line A physical line index
465 * @since jEdit 4.0pre1
466 */
467 public void collapseFold(int line)
468 {
469 int lineCount = buffer.getLineCount();
470 int start = 0;
471 int end = lineCount - 1;
472
473 try
474 {
475 buffer.writeLock();
476
477 // if the caret is on a collapsed fold, collapse the
478 // parent fold
479 if(line != 0
480 && line != buffer.getLineCount() - 1
481 && buffer.isFoldStart(line)
482 && !offsetMgr.isLineVisible(line + 1,index))
483 {
484 line--;
485 }
486
487 int initialFoldLevel = buffer.getFoldLevel(line);
488
489 //{{{ Find fold start and end...
490 if(line != lineCount - 1
491 && buffer.getFoldLevel(line + 1) > initialFoldLevel)
492 {
493 // this line is the start of a fold
494 start = line + 1;
495
496 for(int i = line + 1; i < lineCount; i++)
497 {
498 if(buffer.getFoldLevel(i) <= initialFoldLevel)
499 {
500 end = i - 1;
501 break;
502 }
503 }
504 }
505 else
506 {
507 boolean ok = false;
508
509 // scan backwards looking for the start
510 for(int i = line - 1; i >= 0; i--)
511 {
512 if(buffer.getFoldLevel(i) < initialFoldLevel)
513 {
514 start = i + 1;
515 ok = true;
516 break;
517 }
518 }
519
520 if(!ok)
521 {
522 // no folds in buffer
523 return;
524 }
525
526 for(int i = line + 1; i < lineCount; i++)
527 {
528 if(buffer.getFoldLevel(i) < initialFoldLevel)
529 {
530 end = i - 1;
531 break;
532 }
533 }
534 } //}}}
535
536 //{{{ Collapse the fold...
537 int delta = (end - start + 1);
538
539 for(int i = start; i <= end; i++)
540 {
541 if(offsetMgr.isLineVisible(i,index))
542 offsetMgr.setLineVisible(i,index,false);
543 else
544 delta--;
545 }
546
547 if(delta == 0)
548 {
549 // user probably pressed A+BACK_SPACE twice
550 return;
551 }
552
553 offsetMgr.setVirtualLineCount(index,
554 offsetMgr.getVirtualLineCount(index)
555 - delta);
556 //}}}
557 }
558 finally
559 {
560 buffer.writeUnlock();
561 }
562
563 foldStructureChanged();
564
565 int virtualLine = physicalToVirtual(start);
566 if(textArea.getFirstLine() > virtualLine)
567 textArea.setFirstLine(virtualLine - textArea.getElectricScroll());
568 } //}}}
569
570 //{{{ expandFold() method
571 /**
572 * Expands the fold at the specified physical line index.
573 * @param line A physical line index
574 * @param fully If true, all subfolds will also be expanded
575 * @since jEdit 4.0pre3
576 */
577 public int expandFold(int line, boolean fully)
578 {
579 // the first sub-fold. used by JEditTextArea.expandFold().
580 int returnValue = -1;
581
582 int lineCount = buffer.getLineCount();
583 int start = 0;
584 int end = lineCount - 1;
585 int delta = 0;
586
587 try
588 {
589 buffer.writeLock();
590
591 int initialFoldLevel = buffer.getFoldLevel(line);
592
593 //{{{ Find fold start and fold end...
594 if(line != lineCount - 1
595 && offsetMgr.isLineVisible(line,index)
596 && !offsetMgr.isLineVisible(line + 1,index)
597 && buffer.getFoldLevel(line + 1) > initialFoldLevel)
598 {
599 // this line is the start of a fold
600 start = line + 1;
601
602 for(int i = line + 1; i < lineCount; i++)
603 {
604 if(/* offsetMgr.isLineVisible(i,index) && */
605 buffer.getFoldLevel(i) <= initialFoldLevel)
606 {
607 end = i - 1;
608 break;
609 }
610 }
611 }
612 else
613 {
614 boolean ok = false;
615
616 // scan backwards looking for the start
617 for(int i = line - 1; i >= 0; i--)
618 {
619 if(offsetMgr.isLineVisible(i,index) && buffer.getFoldLevel(i) < initialFoldLevel)
620 {
621 start = i + 1;
622 ok = true;
623 break;
624 }
625 }
626
627 if(!ok)
628 {
629 // no folds in buffer
630 return -1;
631 }
632
633 for(int i = line + 1; i < lineCount; i++)
634 {
635 if((offsetMgr.isLineVisible(i,index) &&
636 buffer.getFoldLevel(i) < initialFoldLevel)
637 || i == getLastVisibleLine())
638 {
639 end = i - 1;
640 break;
641 }
642 }
643 } //}}}
644
645 //{{{ Expand the fold...
646
647 // we need a different value of initialFoldLevel here!
648 initialFoldLevel = buffer.getFoldLevel(start);
649
650 for(int i = start; i <= end; i++)
651 {
652 buffer.getFoldLevel(i);
653 }
654
655 for(int i = start; i <= end; i++)
656 {
657 if(buffer.getFoldLevel(i) > initialFoldLevel)
658 {
659 if(returnValue == -1
660 && i != 0
661 && buffer.isFoldStart(i - 1))
662 {
663 returnValue = i - 1;
664 }
665
666 if(!offsetMgr.isLineVisible(i,index) && fully)
667 {
668 delta++;
669 offsetMgr.setLineVisible(i,index,true);
670 }
671 }
672 else if(!offsetMgr.isLineVisible(i,index))
673 {
674 delta++;
675 offsetMgr.setLineVisible(i,index,true);
676 }
677 }
678
679 offsetMgr.setVirtualLineCount(index,
680 offsetMgr.getVirtualLineCount(index)
681 + delta);
682 //}}}
683
684 if(!fully && !offsetMgr.isLineVisible(line,index))
685 {
686 // this is a hack, and really needs to be done better.
687 expandFold(line,false);
688 return returnValue;
689 }
690 }
691 finally
692 {
693 buffer.writeUnlock();
694 }
695
696 foldStructureChanged();
697
698 int virtualLine = physicalToVirtual(start);
699 int firstLine = textArea.getFirstLine();
700 int visibleLines = textArea.getVisibleLines();
701 if(virtualLine + delta >= firstLine + visibleLines
702 && delta < visibleLines - 1)
703 {
704 textArea.setFirstLine(virtualLine + delta - visibleLines + 1);
705 }
706
707 return returnValue;
708 } //}}}
709
710 //{{{ expandAllFolds() method
711 /**
712 * Expands all folds.
713 * @since jEdit 4.0pre1
714 */
715 public void expandAllFolds()
716 {
717 try
718 {
719 buffer.writeLock();
720
721 narrowed = false;
722
723 if(offsetMgr.getVirtualLineCount(index) == buffer.getLineCount())
724 return;
725
726 offsetMgr.setVirtualLineCount(index,buffer.getLineCount());
727 for(int i = 0; i < buffer.getLineCount(); i++)
728 {
729 offsetMgr.setLineVisible(i,index,true);
730 }
731 foldStructureChanged();
732 }
733 finally
734 {
735 buffer.writeUnlock();
736 }
737 } //}}}
738
739 //{{{ expandFolds() method
740 /**
741 * This method should only be called from <code>actions.xml</code>.
742 * @since jEdit 4.0pre1
743 */
744 public void expandFolds(char digit)
745 {
746 if(digit < '1' || digit > '9')
747 {
748 Toolkit.getDefaultToolkit().beep();
749 return;
750 }
751 else
752 expandFolds((int)(digit - '1') + 1);
753 } //}}}
754
755 //{{{ expandFolds() method
756 /**
757 * Expands all folds with the specified fold level.
758 * @param foldLevel The fold level
759 * @since jEdit 4.0pre1
760 */
761 public void expandFolds(int foldLevel)
762 {
763 try
764 {
765 buffer.writeLock();
766
767 narrowed = false;
768
769 // so that getFoldLevel() calling fireFoldLevelsChanged()
770 // won't break
771 offsetMgr.setVirtualLineCount(index,buffer.getLineCount());
772
773 int newVirtualLineCount = 0;
774 foldLevel = (foldLevel - 1) * buffer.getIndentSize() + 1;
775
776 /* this ensures that the first line is always visible */
777 boolean seenVisibleLine = false;
778
779 for(int i = 0; i < buffer.getLineCount(); i++)
780 {
781 if(!seenVisibleLine || buffer.getFoldLevel(i) < foldLevel)
782 {
783 seenVisibleLine = true;
784 offsetMgr.setLineVisible(i,index,true);
785 newVirtualLineCount++;
786 }
787 else
788 offsetMgr.setLineVisible(i,index,false);
789 }
790
791 offsetMgr.setVirtualLineCount(index,newVirtualLineCount);
792 }
793 finally
794 {
795 buffer.writeUnlock();
796 }
797
798 foldStructureChanged();
799 } //}}}
800
801 //{{{ narrow() method
802 /**
803 * Narrows the visible portion of the buffer to the specified
804 * line range.
805 * @param start The first line
806 * @param end The last line
807 * @since jEdit 4.0pre1
808 */
809 public void narrow(int start, int end)
810 {
811 if(start > end || start < 0 || end >= offsetMgr.getLineCount())
812 throw new ArrayIndexOutOfBoundsException(start + ", " + end);
813
814 if(start < getFirstVisibleLine() || end > getLastVisibleLine())
815 expandAllFolds();
816 // ideally, this should somehow be rolled into the below loop.
817 else if(start != offsetMgr.getLineCount() - 1
818 && !offsetMgr.isLineVisible(start + 1,index))
819 expandFold(start,false);
820
821 int virtualLineCount = offsetMgr.getVirtualLineCount(index);
822 for(int i = 0; i < start; i++)
823 {
824 if(offsetMgr.isLineVisible(i,index))
825 {
826 virtualLineCount--;
827 offsetMgr.setLineVisible(i,index,false);
828 }
829 }
830
831 for(int i = end + 1; i < buffer.getLineCount(); i++)
832 {
833 if(offsetMgr.isLineVisible(i,index))
834 {
835 virtualLineCount--;
836 offsetMgr.setLineVisible(i,index,false);
837 }
838 }
839
840 offsetMgr.setVirtualLineCount(index,virtualLineCount);
841
842 narrowed = true;
843
844 foldStructureChanged();
845
846 // Hack... need a more direct way of obtaining a view?
847 // JEditTextArea.getView() method?
848 GUIUtilities.getView(textArea).getStatus().setMessageAndClear(
849 jEdit.getProperty("view.status.narrow"));
850 } //}}}
851
852 //{{{ Methods for Buffer class to call
853
854 //{{{ _grab() method
855 /**
856 * Do not call this method. The only reason it is public is so
857 * that the <code>Buffer</code> class can call it.
858 */
859 public final void _grab(int index)
860 {
861 this.index = index;
862 lastPhysical = lastVirtual = -1;
863 } //}}}
864
865 //{{{ _release() method
866 /**
867 * Do not call this method. The only reason it is public is so
868 * that the <code>Buffer</code> class can call it.
869 */
870 public final void _release()
871 {
872 index = -1;
873 } //}}}
874
875 //{{{ _getIndex() method
876 /**
877 * Do not call this method. The only reason it is public is so
878 * that the <code>Buffer</code> class can call it.
879 */
880 public final int _getIndex()
881 {
882 return index;
883 } //}}}
884
885 //{{{ _invalidate() method
886 /**
887 * Do not call this method. The only reason it is public is so
888 * that the <code>Buffer</code> class can call it.
889 */
890 public void _invalidate(int startLine)
891 {
892 if(lastPhysical >= startLine)
893 lastPhysical = lastVirtual = -1;
894 } //}}}
895
896 //}}}
897
898 //{{{ foldStructureChanged() method
899 /**
900 * This method is only public so that the EditPane class can call it in
901 * response to a buffer's fold handler change.
902 * @since jEdit 4.0pre8
903 */
904 public void foldStructureChanged()
905 {
906 lastPhysical = lastVirtual = -1;
907 textArea.foldStructureChanged();
908 } //}}}
909
910 //{{{ Private members
911
912 //{{{ Instance variables
913 private Buffer buffer;
914 private OffsetManager offsetMgr;
915 private JEditTextArea textArea;
916 private int index;
917 private int lastPhysical;
918 private int lastVirtual;
919 private boolean narrowed;
920 //}}}
921
922 //}}}
923 }