2 * Created by IntelliJ IDEA.
6 * To change template for new class use
7 * Code Style | Class Templates options (Tools | IDE Options).
9 package com
.intellij
.openapi
.editor
.impl
;
11 import com
.intellij
.openapi
.application
.ex
.ApplicationManagerEx
;
12 import com
.intellij
.openapi
.diagnostic
.Logger
;
13 import com
.intellij
.openapi
.editor
.FoldRegion
;
14 import com
.intellij
.openapi
.editor
.FoldingGroup
;
15 import com
.intellij
.openapi
.editor
.LogicalPosition
;
16 import com
.intellij
.openapi
.editor
.VisualPosition
;
17 import com
.intellij
.openapi
.editor
.colors
.EditorColors
;
18 import com
.intellij
.openapi
.editor
.event
.DocumentEvent
;
19 import com
.intellij
.openapi
.editor
.ex
.DocumentEx
;
20 import com
.intellij
.openapi
.editor
.ex
.FoldingModelEx
;
21 import com
.intellij
.openapi
.editor
.ex
.PrioritizedDocumentListener
;
22 import com
.intellij
.openapi
.editor
.markup
.TextAttributes
;
23 import com
.intellij
.util
.ArrayUtil
;
24 import com
.intellij
.util
.containers
.CollectionFactory
;
25 import com
.intellij
.util
.containers
.MultiMap
;
26 import org
.jetbrains
.annotations
.NotNull
;
30 import java
.util
.List
;
32 public class FoldingModelImpl
implements FoldingModelEx
, PrioritizedDocumentListener
{
33 private static final Logger LOG
= Logger
.getInstance("#com.intellij.openapi.editor.impl.EditorFoldingModelImpl");
34 private boolean myIsFoldingEnabled
;
35 private final EditorImpl myEditor
;
36 private final FoldRegionsTree myFoldTree
;
37 private TextAttributes myFoldTextAttributes
;
38 private boolean myIsBatchFoldingProcessing
;
39 private boolean myDoNotCollapseCaret
;
40 private boolean myFoldRegionsProcessed
;
42 private int mySavedCaretX
;
43 private int mySavedCaretY
;
44 private int mySavedCaretShift
;
45 private boolean myCaretPositionSaved
;
46 private final MultiMap
<FoldingGroup
, FoldRegion
> myGroups
= new MultiMap
<FoldingGroup
, FoldRegion
>();
47 private static final Comparator
<?
super FoldRegion
> OUR_COMPARATOR
= new Comparator
<FoldRegion
>() {
48 public int compare(final FoldRegion o1
, final FoldRegion o2
) {
49 return o1
.getStartOffset() - o2
.getStartOffset();
53 public FoldingModelImpl(EditorImpl editor
) {
55 myIsFoldingEnabled
= true;
56 myIsBatchFoldingProcessing
= false;
57 myDoNotCollapseCaret
= false;
58 myFoldTree
= new FoldRegionsTree();
59 myFoldRegionsProcessed
= false;
64 public List
<FoldRegion
> getGroupedRegions(@NotNull FoldingGroup group
) {
65 return (List
<FoldRegion
>)myGroups
.get(group
);
69 public FoldRegion
getFirstRegion(@NotNull FoldingGroup group
) {
70 final List
<FoldRegion
> regions
= getGroupedRegions(group
);
71 FoldRegion main
= regions
.get(0);
72 for (int i
= 1; i
< regions
.size(); i
++) {
73 FoldRegion region
= regions
.get(i
);
74 if (main
.getStartOffset() > region
.getStartOffset()) {
81 public int getEndOffset(@NotNull FoldingGroup group
) {
82 final List
<FoldRegion
> regions
= getGroupedRegions(group
);
83 int endOffset
= regions
.get(0).getEndOffset();
84 for (int i
= 1; i
< regions
.size(); i
++) {
85 endOffset
= Math
.max(endOffset
, regions
.get(i
).getEndOffset());
90 public void refreshSettings() {
91 myFoldTextAttributes
= myEditor
.getColorsScheme().getAttributes(EditorColors
.FOLDED_TEXT_ATTRIBUTES
);
94 public boolean isFoldingEnabled() {
95 return myIsFoldingEnabled
;
98 public boolean isOffsetCollapsed(int offset
) {
99 assertIsDispatchThread();
100 return getCollapsedRegionAtOffset(offset
) != null;
103 private void assertIsDispatchThread() {
104 ApplicationManagerEx
.getApplicationEx().assertIsDispatchThread(myEditor
.getComponent());
107 public void setFoldingEnabled(boolean isEnabled
) {
108 assertIsDispatchThread();
109 myIsFoldingEnabled
= isEnabled
;
112 public FoldRegion
addFoldRegion(int startOffset
, int endOffset
, @NotNull String placeholderText
) {
113 final FoldRegionImpl region
= new FoldRegionImpl(myEditor
, startOffset
, endOffset
, placeholderText
, null);
114 return addFoldRegion(region
) ? region
: null;
117 public boolean addFoldRegion(@NotNull final FoldRegion region
) {
118 assertIsDispatchThread();
119 if (isFoldingEnabled()) {
120 if (!myIsBatchFoldingProcessing
) {
121 LOG
.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
125 myFoldRegionsProcessed
= true;
126 if (myFoldTree
.addRegion(region
)) {
127 final FoldingGroup group
= region
.getGroup();
129 myGroups
.putValue(group
, region
);
138 public void runBatchFoldingOperation(Runnable operation
) {
139 runBatchFoldingOperation(operation
, false);
142 private void runBatchFoldingOperation(final Runnable operation
, final boolean dontCollapseCaret
) {
143 assertIsDispatchThread();
144 boolean oldDontCollapseCaret
= myDoNotCollapseCaret
;
145 myDoNotCollapseCaret
|= dontCollapseCaret
;
146 boolean oldBatchFlag
= myIsBatchFoldingProcessing
;
148 mySavedCaretShift
= myEditor
.visibleLineNumberToYPosition(myEditor
.getCaretModel().getVisualPosition().line
) - myEditor
.getScrollingModel().getVerticalScrollOffset();
151 myIsBatchFoldingProcessing
= true;
152 myFoldTree
.myCachedLastIndex
= -1;
154 myFoldTree
.myCachedLastIndex
= -1;
157 if (myFoldRegionsProcessed
) {
158 notifyBatchFoldingProcessingDone();
159 myFoldRegionsProcessed
= false;
161 myIsBatchFoldingProcessing
= false;
163 myDoNotCollapseCaret
= oldDontCollapseCaret
;
166 public void runBatchFoldingOperationDoNotCollapseCaret(final Runnable operation
) {
167 runBatchFoldingOperation(operation
, true);
170 public void flushCaretShift() {
171 mySavedCaretShift
= -1;
175 public FoldRegion
[] getAllFoldRegions() {
176 assertIsDispatchThread();
177 return myFoldTree
.fetchAllRegions();
180 public FoldRegion
getCollapsedRegionAtOffset(int offset
) {
181 return myFoldTree
.fetchOutermost(offset
);
184 int getLastTopLevelIndexBefore (int offset
) {
185 return myFoldTree
.getLastTopLevelIndexBefore(offset
);
188 public FoldRegion
getFoldingPlaceholderAt(Point p
) {
189 assertIsDispatchThread();
190 LogicalPosition pos
= myEditor
.xyToLogicalPosition(p
);
193 if (line
>= myEditor
.getDocument().getLineCount()) return null;
195 //leftmost folded block position
196 if (myEditor
.xyToVisualPosition(p
).equals(myEditor
.logicalToVisualPosition(pos
))) return null;
198 int offset
= myEditor
.logicalPositionToOffset(pos
);
200 return myFoldTree
.fetchOutermost(offset
);
203 public FoldRegion
[] getAllFoldRegionsIncludingInvalid() {
204 assertIsDispatchThread();
205 return myFoldTree
.fetchAllRegionsIncludingInvalid();
208 public void removeFoldRegion(@NotNull final FoldRegion region
) {
209 assertIsDispatchThread();
211 if (!myIsBatchFoldingProcessing
) {
212 LOG
.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
215 region
.setExpanded(true);
216 final FoldingGroup group
= region
.getGroup();
218 myGroups
.removeValue(group
, region
);
220 myFoldTree
.removeRegion(region
);
221 myFoldRegionsProcessed
= true;
224 public void expandFoldRegion(FoldRegion region
) {
225 assertIsDispatchThread();
226 if (region
.isExpanded()) return;
228 if (!myIsBatchFoldingProcessing
) {
229 LOG
.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
232 if (myCaretPositionSaved
) {
233 int savedOffset
= myEditor
.logicalPositionToOffset(new LogicalPosition(mySavedCaretY
, mySavedCaretX
));
235 FoldRegion
[] allCollapsed
= myFoldTree
.fetchCollapsedAt(savedOffset
);
236 if (allCollapsed
.length
== 1 && allCollapsed
[0] == region
) {
237 LogicalPosition pos
= new LogicalPosition(mySavedCaretY
, mySavedCaretX
);
238 myEditor
.getCaretModel().moveToLogicalPosition(pos
);
242 myFoldRegionsProcessed
= true;
243 ((FoldRegionImpl
) region
).setExpandedInternal(true);
246 public void collapseFoldRegion(FoldRegion region
) {
247 assertIsDispatchThread();
248 if (!region
.isExpanded()) return;
250 if (!myIsBatchFoldingProcessing
) {
251 LOG
.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
254 LogicalPosition caretPosition
= myEditor
.getCaretModel().getLogicalPosition();
256 int caretOffset
= myEditor
.logicalPositionToOffset(caretPosition
);
258 if (myFoldTree
.contains(region
, caretOffset
)) {
259 if (myDoNotCollapseCaret
) return;
261 if (!myCaretPositionSaved
) {
262 mySavedCaretX
= caretPosition
.column
;
263 mySavedCaretY
= caretPosition
.line
;
264 myCaretPositionSaved
= true;
268 int selectionStart
= myEditor
.getSelectionModel().getSelectionStart();
269 int selectionEnd
= myEditor
.getSelectionModel().getSelectionEnd();
271 if (myFoldTree
.contains(region
, selectionStart
-1) || myFoldTree
.contains(region
, selectionEnd
)) myEditor
.getSelectionModel().removeSelection();
273 myFoldRegionsProcessed
= true;
274 ((FoldRegionImpl
) region
).setExpandedInternal(false);
277 private void notifyBatchFoldingProcessingDone() {
278 myFoldTree
.rebuild();
280 myEditor
.updateCaretCursor();
281 myEditor
.recalcSizeAndRepaint();
282 if (myEditor
.getGutterComponentEx().isFoldingOutlineShown()) {
283 myEditor
.getGutterComponentEx().repaint();
286 LogicalPosition caretPosition
= myEditor
.getCaretModel().getLogicalPosition();
287 int caretOffset
= myEditor
.logicalPositionToOffset(caretPosition
);
288 boolean hasBlockSelection
= myEditor
.getSelectionModel().hasBlockSelection();
289 int selectionStart
= myEditor
.getSelectionModel().getSelectionStart();
290 int selectionEnd
= myEditor
.getSelectionModel().getSelectionEnd();
295 FoldRegion collapsed
= myFoldTree
.fetchOutermost(caretOffset
);
296 if (myCaretPositionSaved
) {
297 int savedOffset
= myEditor
.logicalPositionToOffset(new LogicalPosition(mySavedCaretY
, mySavedCaretX
));
298 FoldRegion collapsedAtSaved
= myFoldTree
.fetchOutermost(savedOffset
);
299 column
= mySavedCaretX
;
300 line
= collapsedAtSaved
!= null ? collapsedAtSaved
.getDocument().getLineNumber(collapsedAtSaved
.getStartOffset()) : mySavedCaretY
;
303 if (collapsed
!= null && column
== -1) {
304 line
= collapsed
.getDocument().getLineNumber(collapsed
.getStartOffset());
305 column
= myEditor
.getCaretModel().getVisualPosition().column
;
308 boolean oldCaretPositionSaved
= myCaretPositionSaved
;
311 LogicalPosition log
= new LogicalPosition(line
, 0);
312 VisualPosition vis
= myEditor
.logicalToVisualPosition(log
);
313 VisualPosition pos
= new VisualPosition(vis
.line
, column
);
314 myEditor
.getCaretModel().moveToVisualPosition(pos
);
316 myEditor
.getCaretModel().moveToLogicalPosition(caretPosition
);
319 myCaretPositionSaved
= oldCaretPositionSaved
;
321 if (!hasBlockSelection
) {
322 myEditor
.getSelectionModel().setSelection(selectionStart
, selectionEnd
);
325 if (mySavedCaretShift
> 0) {
326 myEditor
.getScrollingModel().disableAnimation();
327 int scrollTo
= myEditor
.visibleLineNumberToYPosition(myEditor
.getCaretModel().getVisualPosition().line
) - mySavedCaretShift
;
328 myEditor
.getScrollingModel().scrollVertically(scrollTo
);
329 myEditor
.getScrollingModel().enableAnimation();
333 public void rebuild() {
334 myFoldTree
.rebuild();
337 private void updateCachedOffsets() {
338 myFoldTree
.updateCachedOffsets();
341 public int getFoldedLinesCountBefore(int offset
) {
342 return myFoldTree
.getFoldedLinesCountBefore(offset
);
345 FoldRegion
[] fetchTopLevel() {
346 return myFoldTree
.fetchTopLevel();
349 FoldRegion
fetchOutermost(int offset
) {
350 return myFoldTree
.fetchOutermost(offset
);
353 public FoldRegion
[] fetchCollapsedAt(int offset
) {
354 return myFoldTree
.fetchCollapsedAt(offset
);
357 public boolean intersectsRegion (int startOffset
, int endOffset
) {
358 return myFoldTree
.intersectsRegion(startOffset
, endOffset
);
361 public FoldRegion
[] fetchVisible() {
362 return myFoldTree
.fetchVisible();
365 public int getLastCollapsedRegionBefore(int offset
) {
366 return myFoldTree
.getLastTopLevelIndexBefore(offset
);
369 public TextAttributes
getPlaceholderAttributes() {
370 return myFoldTextAttributes
;
373 public void flushCaretPosition() {
374 myCaretPositionSaved
= false;
377 class FoldRegionsTree
{
378 private FoldRegion
[] myCachedVisible
;
379 private FoldRegion
[] myCachedTopLevelRegions
;
380 private int[] myCachedEndOffsets
;
381 private int[] myCachedStartOffsets
;
382 private int[] myCachedFoldedLines
;
383 private int myCachedLastIndex
= -1;
384 private ArrayList
<FoldRegion
> myRegions
= CollectionFactory
.arrayList(); //sorted in tree left-to-right topdown traversal order
386 private void clear() {
387 myCachedVisible
= null;
388 myCachedTopLevelRegions
= null;
389 myCachedEndOffsets
= null;
390 myCachedStartOffsets
= null;
391 myCachedFoldedLines
= null;
392 myRegions
= new ArrayList
<FoldRegion
>();
395 private boolean isFoldingEnabled() {
396 return FoldingModelImpl
.this.isFoldingEnabled() && myCachedVisible
!= null;
400 ArrayList
<FoldRegion
> topLevels
= new ArrayList
<FoldRegion
>(myRegions
.size() / 2);
401 ArrayList
<FoldRegion
> visible
= new ArrayList
<FoldRegion
>(myRegions
.size());
402 FoldRegion
[] regions
= myRegions
.toArray(new FoldRegion
[myRegions
.size()]);
403 FoldRegion currentToplevel
= null;
404 for (FoldRegion region
: regions
) {
405 if (region
.isValid()) {
407 if (!region
.isExpanded()) {
408 if (currentToplevel
== null || currentToplevel
.getEndOffset() < region
.getStartOffset()) {
409 currentToplevel
= region
;
410 topLevels
.add(region
);
416 myCachedTopLevelRegions
= topLevels
.isEmpty() ? FoldRegion
.EMPTY_ARRAY
: topLevels
.toArray(new FoldRegion
[topLevels
.size()]);
418 Arrays
.sort(myCachedTopLevelRegions
, new Comparator
<FoldRegion
>() {
419 public int compare(FoldRegion r1
, FoldRegion r2
) {
420 int end1
= r1
.getEndOffset();
421 int end2
= r2
.getEndOffset();
422 if (end1
< end2
) return -1;
423 if (end1
> end2
) return 1;
428 FoldRegion
[] visibleArrayed
= visible
.toArray(new FoldRegion
[visible
.size()]);
429 for (FoldRegion visibleRegion
: visibleArrayed
) {
430 for (FoldRegion topLevelRegion
: myCachedTopLevelRegions
) {
431 if (contains(topLevelRegion
, visibleRegion
)) {
432 visible
.remove(visibleRegion
);
438 myCachedVisible
= visible
.toArray(new FoldRegion
[visible
.size()]);
440 Arrays
.sort(myCachedVisible
, new Comparator
<FoldRegion
>() {
441 public int compare(FoldRegion r1
, FoldRegion r2
) {
442 int end1
= r1
.getEndOffset();
443 int end2
= r2
.getEndOffset();
444 if (end1
< end2
) return 1;
445 if (end1
> end2
) return -1;
450 updateCachedOffsets();
453 void updateCachedOffsets() {
454 if (FoldingModelImpl
.this.isFoldingEnabled()) {
455 if (myCachedVisible
== null) {
460 for (FoldRegion foldRegion
: myCachedVisible
) {
461 if (!foldRegion
.isValid()) {
467 int length
= myCachedTopLevelRegions
.length
;
468 if (myCachedEndOffsets
== null || myCachedEndOffsets
.length
!= length
) {
470 myCachedEndOffsets
= new int[length
];
471 myCachedStartOffsets
= new int[length
];
472 myCachedFoldedLines
= new int[length
];
475 myCachedEndOffsets
= ArrayUtil
.EMPTY_INT_ARRAY
;
476 myCachedStartOffsets
= ArrayUtil
.EMPTY_INT_ARRAY
;
477 myCachedFoldedLines
= ArrayUtil
.EMPTY_INT_ARRAY
;
482 for (int i
= 0; i
< length
; i
++) {
483 FoldRegion region
= myCachedTopLevelRegions
[i
];
484 myCachedStartOffsets
[i
] = region
.getStartOffset();
485 myCachedEndOffsets
[i
] = region
.getEndOffset() - 1;
486 sum
+= region
.getDocument().getLineNumber(region
.getEndOffset()) - region
.getDocument().getLineNumber(region
.getStartOffset());
487 myCachedFoldedLines
[i
] = sum
;
492 boolean addRegion(FoldRegion range
) {
493 // During batchProcessing elements are inserted in ascending order,
494 // binary search find acceptable insertion place first time
495 int fastIndex
= myCachedLastIndex
!= -1 && myIsBatchFoldingProcessing? myCachedLastIndex
+ 1:Collections
.binarySearch(myRegions
, range
, OUR_COMPARATOR
);
496 if (fastIndex
< 0) fastIndex
= -fastIndex
- 1;
498 for (int i
= fastIndex
- 1; i
>=0; --i
) {
499 final FoldRegion region
= myRegions
.get(i
);
500 if (region
.getEndOffset() < range
.getStartOffset()) break;
501 if (region
.isValid() && intersects(region
, range
)) {
506 for (int i
= fastIndex
; i
< myRegions
.size(); i
++) {
507 final FoldRegion region
= myRegions
.get(i
);
509 if (range
.getStartOffset() < region
.getStartOffset() ||
510 range
.getStartOffset() == region
.getStartOffset() && range
.getEndOffset() > region
.getEndOffset()) {
511 for (int j
= i
+ 1; j
< myRegions
.size(); j
++) {
512 final FoldRegion next
= myRegions
.get(j
);
513 if (next
.getEndOffset() >= range
.getEndOffset() && next
.isValid()) {
514 if (next
.getStartOffset() < range
.getStartOffset()) {
523 myRegions
.add(myCachedLastIndex
= i
, range
);
527 myRegions
.add(myCachedLastIndex
= myRegions
.size(),range
);
531 FoldRegion
fetchOutermost(int offset
) {
532 if (!isFoldingEnabled()) return null;
534 final int[] starts
= myCachedStartOffsets
;
535 final int[] ends
= myCachedEndOffsets
;
538 int end
= ends
.length
- 1;
540 while (start
<= end
) {
541 int i
= (start
+ end
) / 2;
542 if (offset
< starts
[i
]) {
544 } else if (offset
> ends
[i
]) {
548 return myCachedTopLevelRegions
[i
];
555 FoldRegion
[] fetchVisible() {
556 if (!isFoldingEnabled()) return new FoldRegion
[0];
557 return myCachedVisible
;
560 FoldRegion
[] fetchTopLevel() {
561 if (!isFoldingEnabled()) return null;
562 return myCachedTopLevelRegions
;
565 private boolean contains(FoldRegion outer
, FoldRegion inner
) {
566 return outer
.getStartOffset() < inner
.getStartOffset() && outer
.getEndOffset() > inner
.getStartOffset();
569 private boolean intersects(FoldRegion r1
, FoldRegion r2
) {
570 final int s1
= r1
.getStartOffset();
571 final int s2
= r2
.getStartOffset();
572 final int e1
= r1
.getEndOffset();
573 final int e2
= r2
.getEndOffset();
574 return (s1
== s2
&& e1
== e2
) ||
575 (s1
< s2
&& s2
< e1
&& e1
< e2
) ||
576 (s2
< s1
&& s1
< e2
&& e2
< e1
);
579 private boolean contains(FoldRegion region
, int offset
) {
580 return region
.getStartOffset() < offset
&& region
.getEndOffset() > offset
;
583 public FoldRegion
[] fetchCollapsedAt(int offset
) {
584 if (!isFoldingEnabled()) return new FoldRegion
[0];
585 ArrayList
<FoldRegion
> allCollapsed
= new ArrayList
<FoldRegion
>();
586 for (FoldRegion region
: myRegions
) {
587 if (!region
.isExpanded() && contains(region
, offset
)) {
588 allCollapsed
.add(region
);
592 return allCollapsed
.toArray(new FoldRegion
[allCollapsed
.size()]);
595 boolean intersectsRegion(int startOffset
, int endOffset
) {
596 if (!FoldingModelImpl
.this.isFoldingEnabled()) return true;
597 for (FoldRegion region
: myRegions
) {
598 boolean contains1
= contains(region
, startOffset
);
599 boolean contains2
= contains(region
, endOffset
);
600 if (contains1
!= contains2
) {
607 FoldRegion
[] fetchAllRegions() {
608 if (!isFoldingEnabled()) return new FoldRegion
[0];
610 return myRegions
.toArray(new FoldRegion
[myRegions
.size()]);
613 void removeRegion(FoldRegion range
) {
614 myRegions
.remove(range
);
617 int getFoldedLinesCountBefore(int offset
) {
618 int idx
= getLastTopLevelIndexBefore(offset
);
619 if (idx
== -1) return 0;
620 return myCachedFoldedLines
[idx
];
623 public int getLastTopLevelIndexBefore(int offset
) {
624 if (!isFoldingEnabled()) return -1;
627 int end
= myCachedEndOffsets
.length
- 1;
629 while (start
<= end
) {
630 int i
= (start
+ end
) / 2;
631 if (offset
< myCachedEndOffsets
[i
]) {
633 } else if (offset
> myCachedEndOffsets
[i
]) {
643 // for (int i = 0; i < myCachedEndOffsets.length; i++) {
644 // if (!myCachedTopLevelRegions[i].isValid()) continue;
645 // int endOffset = myCachedEndOffsets[i];
646 // if (endOffset > offset) break;
653 public FoldRegion
[] fetchAllRegionsIncludingInvalid() {
654 if (!FoldingModelImpl
.this.isFoldingEnabled()) return new FoldRegion
[0];
656 return myRegions
.toArray(new FoldRegion
[myRegions
.size()]);
660 public void beforeDocumentChange(DocumentEvent event
) {
663 public void documentChanged(DocumentEvent event
) {
664 if (((DocumentEx
)event
.getDocument()).isInBulkUpdate()) {
667 updateCachedOffsets();
671 public int getPriority() {