IDEA-51739
[fedora-idea.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / FoldingModelImpl.java
blob5f364de8d748af9c2c2154cae1e91f7bb502dcee
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * Created by IntelliJ IDEA.
19 * User: max
20 * Date: Jun 4, 2002
21 * Time: 8:27:13 PM
22 * To change template for new class use
23 * Code Style | Class Templates options (Tools | IDE Options).
25 package com.intellij.openapi.editor.impl;
27 import com.intellij.openapi.application.ex.ApplicationManagerEx;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.editor.FoldRegion;
30 import com.intellij.openapi.editor.FoldingGroup;
31 import com.intellij.openapi.editor.LogicalPosition;
32 import com.intellij.openapi.editor.VisualPosition;
33 import com.intellij.openapi.editor.colors.EditorColors;
34 import com.intellij.openapi.editor.event.DocumentEvent;
35 import com.intellij.openapi.editor.ex.DocumentEx;
36 import com.intellij.openapi.editor.ex.FoldingModelEx;
37 import com.intellij.openapi.editor.ex.PrioritizedDocumentListener;
38 import com.intellij.openapi.editor.markup.TextAttributes;
39 import com.intellij.util.ArrayUtil;
40 import com.intellij.util.containers.CollectionFactory;
41 import com.intellij.util.containers.MultiMap;
42 import org.jetbrains.annotations.NotNull;
44 import java.awt.*;
45 import java.util.*;
46 import java.util.List;
48 public class FoldingModelImpl implements FoldingModelEx, PrioritizedDocumentListener {
49 private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorFoldingModelImpl");
50 private boolean myIsFoldingEnabled;
51 private final EditorImpl myEditor;
52 private final FoldRegionsTree myFoldTree;
53 private TextAttributes myFoldTextAttributes;
54 private boolean myIsBatchFoldingProcessing;
55 private boolean myDoNotCollapseCaret;
56 private boolean myFoldRegionsProcessed;
58 private int mySavedCaretX;
59 private int mySavedCaretY;
60 private int mySavedCaretShift;
61 private boolean myCaretPositionSaved;
62 private final MultiMap<FoldingGroup, FoldRegion> myGroups = new MultiMap<FoldingGroup, FoldRegion>();
63 private static final Comparator<? super FoldRegion> OUR_COMPARATOR = new Comparator<FoldRegion>() {
64 public int compare(final FoldRegion o1, final FoldRegion o2) {
65 return o1.getStartOffset() - o2.getStartOffset();
68 private static final Comparator<FoldRegion> BY_END_OFFSET = new Comparator<FoldRegion>() {
69 public int compare(FoldRegion r1, FoldRegion r2) {
70 int end1 = r1.getEndOffset();
71 int end2 = r2.getEndOffset();
72 if (end1 < end2) return -1;
73 if (end1 > end2) return 1;
74 return 0;
77 private static final Comparator<? super FoldRegion> BY_END_OFFSET_REVERSE = Collections.reverseOrder(BY_END_OFFSET);
79 public FoldingModelImpl(EditorImpl editor) {
80 myEditor = editor;
81 myIsFoldingEnabled = true;
82 myIsBatchFoldingProcessing = false;
83 myDoNotCollapseCaret = false;
84 myFoldTree = new FoldRegionsTree();
85 myFoldRegionsProcessed = false;
86 refreshSettings();
89 @NotNull
90 public List<FoldRegion> getGroupedRegions(@NotNull FoldingGroup group) {
91 return (List<FoldRegion>)myGroups.get(group);
94 @NotNull
95 public FoldRegion getFirstRegion(@NotNull FoldingGroup group) {
96 final List<FoldRegion> regions = getGroupedRegions(group);
97 FoldRegion main = regions.get(0);
98 for (int i = 1; i < regions.size(); i++) {
99 FoldRegion region = regions.get(i);
100 if (main.getStartOffset() > region.getStartOffset()) {
101 main = region;
104 return main;
107 public int getEndOffset(@NotNull FoldingGroup group) {
108 final List<FoldRegion> regions = getGroupedRegions(group);
109 int endOffset = regions.get(0).getEndOffset();
110 for (int i = 1; i < regions.size(); i++) {
111 if (regions.get(i).isValid()) {
112 endOffset = Math.max(endOffset, regions.get(i).getEndOffset());
115 return endOffset;
118 public void refreshSettings() {
119 myFoldTextAttributes = myEditor.getColorsScheme().getAttributes(EditorColors.FOLDED_TEXT_ATTRIBUTES);
122 public boolean isFoldingEnabled() {
123 return myIsFoldingEnabled;
126 public boolean isOffsetCollapsed(int offset) {
127 assertReadAccess();
128 return getCollapsedRegionAtOffset(offset) != null;
131 private void assertIsDispatchThread() {
132 ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(myEditor.getComponent());
134 private static void assertReadAccess() {
135 ApplicationManagerEx.getApplicationEx().assertReadAccessAllowed();
138 public void setFoldingEnabled(boolean isEnabled) {
139 assertIsDispatchThread();
140 myIsFoldingEnabled = isEnabled;
143 public FoldRegion addFoldRegion(int startOffset, int endOffset, @NotNull String placeholderText) {
144 final FoldRegionImpl region = new FoldRegionImpl(myEditor, startOffset, endOffset, placeholderText, null);
145 return addFoldRegion(region) ? region : null;
148 public boolean addFoldRegion(@NotNull final FoldRegion region) {
149 assertIsDispatchThread();
150 if (isFoldingEnabled()) {
151 if (!myIsBatchFoldingProcessing) {
152 LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
153 return false;
156 myFoldRegionsProcessed = true;
157 if (myFoldTree.addRegion(region)) {
158 final FoldingGroup group = region.getGroup();
159 if (group != null) {
160 myGroups.putValue(group, region);
162 return true;
166 return false;
169 public void runBatchFoldingOperation(@NotNull Runnable operation) {
170 runBatchFoldingOperation(operation, false);
173 private void runBatchFoldingOperation(final Runnable operation, final boolean dontCollapseCaret) {
174 assertIsDispatchThread();
175 boolean oldDontCollapseCaret = myDoNotCollapseCaret;
176 myDoNotCollapseCaret |= dontCollapseCaret;
177 boolean oldBatchFlag = myIsBatchFoldingProcessing;
178 if (!oldBatchFlag) {
179 mySavedCaretShift = myEditor.visibleLineNumberToYPosition(myEditor.getCaretModel().getVisualPosition().line) - myEditor.getScrollingModel().getVerticalScrollOffset();
182 myIsBatchFoldingProcessing = true;
183 myFoldTree.myCachedLastIndex = -1;
184 operation.run();
185 myFoldTree.myCachedLastIndex = -1;
187 if (!oldBatchFlag) {
188 if (myFoldRegionsProcessed) {
189 notifyBatchFoldingProcessingDone();
190 myFoldRegionsProcessed = false;
192 myIsBatchFoldingProcessing = false;
194 myDoNotCollapseCaret = oldDontCollapseCaret;
197 public void runBatchFoldingOperationDoNotCollapseCaret(@NotNull final Runnable operation) {
198 runBatchFoldingOperation(operation, true);
201 public void flushCaretShift() {
202 mySavedCaretShift = -1;
205 @NotNull
206 public FoldRegion[] getAllFoldRegions() {
207 assertReadAccess();
208 return myFoldTree.fetchAllRegions();
211 public FoldRegion getCollapsedRegionAtOffset(int offset) {
212 return myFoldTree.fetchOutermost(offset);
215 int getLastTopLevelIndexBefore (int offset) {
216 return myFoldTree.getLastTopLevelIndexBefore(offset);
219 public FoldRegion getFoldingPlaceholderAt(Point p) {
220 assertReadAccess();
221 LogicalPosition pos = myEditor.xyToLogicalPosition(p);
222 int line = pos.line;
224 if (line >= myEditor.getDocument().getLineCount()) return null;
226 //leftmost folded block position
227 if (myEditor.xyToVisualPosition(p).equals(myEditor.logicalToVisualPosition(pos))) return null;
229 int offset = myEditor.logicalPositionToOffset(pos);
231 return myFoldTree.fetchOutermost(offset);
234 public FoldRegion[] getAllFoldRegionsIncludingInvalid() {
235 assertReadAccess();
236 return myFoldTree.fetchAllRegionsIncludingInvalid();
239 public void removeFoldRegion(@NotNull final FoldRegion region) {
240 assertIsDispatchThread();
242 if (!myIsBatchFoldingProcessing) {
243 LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
246 region.setExpanded(true);
247 final FoldingGroup group = region.getGroup();
248 if (group != null) {
249 myGroups.removeValue(group, region);
251 myFoldTree.removeRegion(region);
252 myFoldRegionsProcessed = true;
255 public void expandFoldRegion(FoldRegion region) {
256 assertIsDispatchThread();
257 if (region.isExpanded()) return;
259 if (!myIsBatchFoldingProcessing) {
260 LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
263 if (myCaretPositionSaved) {
264 int savedOffset = myEditor.logicalPositionToOffset(new LogicalPosition(mySavedCaretY, mySavedCaretX));
266 FoldRegion[] allCollapsed = myFoldTree.fetchCollapsedAt(savedOffset);
267 if (allCollapsed.length == 1 && allCollapsed[0] == region) {
268 LogicalPosition pos = new LogicalPosition(mySavedCaretY, mySavedCaretX);
269 myEditor.getCaretModel().moveToLogicalPosition(pos);
273 myFoldRegionsProcessed = true;
274 ((FoldRegionImpl) region).setExpandedInternal(true);
277 public void collapseFoldRegion(FoldRegion region) {
278 assertIsDispatchThread();
279 if (!region.isExpanded()) return;
281 if (!myIsBatchFoldingProcessing) {
282 LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
285 LogicalPosition caretPosition = myEditor.getCaretModel().getLogicalPosition();
287 int caretOffset = myEditor.logicalPositionToOffset(caretPosition);
289 if (myFoldTree.contains(region, caretOffset)) {
290 if (myDoNotCollapseCaret) return;
292 if (!myCaretPositionSaved) {
293 mySavedCaretX = caretPosition.column;
294 mySavedCaretY = caretPosition.line;
295 myCaretPositionSaved = true;
299 int selectionStart = myEditor.getSelectionModel().getSelectionStart();
300 int selectionEnd = myEditor.getSelectionModel().getSelectionEnd();
302 if (myFoldTree.contains(region, selectionStart-1) || myFoldTree.contains(region, selectionEnd)) myEditor.getSelectionModel().removeSelection();
304 myFoldRegionsProcessed = true;
305 ((FoldRegionImpl) region).setExpandedInternal(false);
308 private void notifyBatchFoldingProcessingDone() {
309 myFoldTree.rebuild();
311 myEditor.updateCaretCursor();
312 myEditor.recalcSizeAndRepaint();
313 if (myEditor.getGutterComponentEx().isFoldingOutlineShown()) {
314 myEditor.getGutterComponentEx().repaint();
317 LogicalPosition caretPosition = myEditor.getCaretModel().getLogicalPosition();
318 int caretOffset = myEditor.logicalPositionToOffset(caretPosition);
319 boolean hasBlockSelection = myEditor.getSelectionModel().hasBlockSelection();
320 int selectionStart = myEditor.getSelectionModel().getSelectionStart();
321 int selectionEnd = myEditor.getSelectionModel().getSelectionEnd();
323 int column = -1;
324 int line = -1;
326 FoldRegion collapsed = myFoldTree.fetchOutermost(caretOffset);
327 if (myCaretPositionSaved) {
328 int savedOffset = myEditor.logicalPositionToOffset(new LogicalPosition(mySavedCaretY, mySavedCaretX));
329 FoldRegion collapsedAtSaved = myFoldTree.fetchOutermost(savedOffset);
330 column = mySavedCaretX;
331 line = collapsedAtSaved != null ? collapsedAtSaved.getDocument().getLineNumber(collapsedAtSaved.getStartOffset()) : mySavedCaretY;
334 if (collapsed != null && column == -1) {
335 line = collapsed.getDocument().getLineNumber(collapsed.getStartOffset());
336 column = myEditor.getCaretModel().getVisualPosition().column;
339 boolean oldCaretPositionSaved = myCaretPositionSaved;
341 if (column != -1) {
342 LogicalPosition log = new LogicalPosition(line, 0);
343 VisualPosition vis = myEditor.logicalToVisualPosition(log);
344 VisualPosition pos = new VisualPosition(vis.line, column);
345 myEditor.getCaretModel().moveToVisualPosition(pos);
346 } else {
347 myEditor.getCaretModel().moveToLogicalPosition(caretPosition);
350 myCaretPositionSaved = oldCaretPositionSaved;
352 if (!hasBlockSelection) {
353 myEditor.getSelectionModel().setSelection(selectionStart, selectionEnd);
356 if (mySavedCaretShift > 0) {
357 myEditor.getScrollingModel().disableAnimation();
358 int scrollTo = myEditor.visibleLineNumberToYPosition(myEditor.getCaretModel().getVisualPosition().line) - mySavedCaretShift;
359 myEditor.getScrollingModel().scrollVertically(scrollTo);
360 myEditor.getScrollingModel().enableAnimation();
364 public void rebuild() {
365 myFoldTree.rebuild();
368 private void updateCachedOffsets() {
369 myFoldTree.updateCachedOffsets();
372 public int getFoldedLinesCountBefore(int offset) {
373 return myFoldTree.getFoldedLinesCountBefore(offset);
376 public FoldRegion[] fetchTopLevel() {
377 return myFoldTree.fetchTopLevel();
380 public FoldRegion fetchOutermost(int offset) {
381 return myFoldTree.fetchOutermost(offset);
384 public FoldRegion[] fetchCollapsedAt(int offset) {
385 return myFoldTree.fetchCollapsedAt(offset);
388 public boolean intersectsRegion (int startOffset, int endOffset) {
389 return myFoldTree.intersectsRegion(startOffset, endOffset);
392 public FoldRegion[] fetchVisible() {
393 return myFoldTree.fetchVisible();
396 public int getLastCollapsedRegionBefore(int offset) {
397 return myFoldTree.getLastTopLevelIndexBefore(offset);
400 public TextAttributes getPlaceholderAttributes() {
401 return myFoldTextAttributes;
404 public void flushCaretPosition() {
405 myCaretPositionSaved = false;
408 class FoldRegionsTree {
409 private FoldRegion[] myCachedVisible;
410 private FoldRegion[] myCachedTopLevelRegions;
411 private int[] myCachedEndOffsets;
412 private int[] myCachedStartOffsets;
413 private int[] myCachedFoldedLines;
414 private int myCachedLastIndex = -1;
415 private ArrayList<FoldRegion> myRegions = CollectionFactory.arrayList(); //sorted in tree left-to-right topdown traversal order
417 private void clear() {
418 myCachedVisible = null;
419 myCachedTopLevelRegions = null;
420 myCachedEndOffsets = null;
421 myCachedStartOffsets = null;
422 myCachedFoldedLines = null;
423 myRegions = new ArrayList<FoldRegion>();
426 private boolean isFoldingEnabled() {
427 return FoldingModelImpl.this.isFoldingEnabled() && myCachedVisible != null;
430 void rebuild() {
431 ArrayList<FoldRegion> topLevels = new ArrayList<FoldRegion>(myRegions.size() / 2);
432 ArrayList<FoldRegion> visible = new ArrayList<FoldRegion>(myRegions.size());
433 FoldRegion[] regions = myRegions.toArray(new FoldRegion[myRegions.size()]);
434 FoldRegion currentToplevel = null;
435 for (FoldRegion region : regions) {
436 if (region.isValid()) {
437 visible.add(region);
438 if (!region.isExpanded()) {
439 if (currentToplevel == null || currentToplevel.getEndOffset() < region.getStartOffset()) {
440 currentToplevel = region;
441 topLevels.add(region);
447 myCachedTopLevelRegions = topLevels.isEmpty() ? FoldRegion.EMPTY_ARRAY : topLevels.toArray(new FoldRegion[topLevels.size()]);
449 Arrays.sort(myCachedTopLevelRegions, BY_END_OFFSET);
451 FoldRegion[] visibleArrayed = visible.toArray(new FoldRegion[visible.size()]);
452 for (FoldRegion visibleRegion : visibleArrayed) {
453 for (FoldRegion topLevelRegion : myCachedTopLevelRegions) {
454 if (contains(topLevelRegion, visibleRegion)) {
455 visible.remove(visibleRegion);
456 break;
461 myCachedVisible = visible.toArray(new FoldRegion[visible.size()]);
463 Arrays.sort(myCachedVisible, BY_END_OFFSET_REVERSE);
465 updateCachedOffsets();
468 void updateCachedOffsets() {
469 if (FoldingModelImpl.this.isFoldingEnabled()) {
470 if (myCachedVisible == null) {
471 rebuild();
472 return;
475 for (FoldRegion foldRegion : myCachedVisible) {
476 if (!foldRegion.isValid()) {
477 rebuild();
478 return;
482 int length = myCachedTopLevelRegions.length;
483 if (myCachedEndOffsets == null || myCachedEndOffsets.length != length) {
484 if (length != 0) {
485 myCachedEndOffsets = new int[length];
486 myCachedStartOffsets = new int[length];
487 myCachedFoldedLines = new int[length];
489 else {
490 myCachedEndOffsets = ArrayUtil.EMPTY_INT_ARRAY;
491 myCachedStartOffsets = ArrayUtil.EMPTY_INT_ARRAY;
492 myCachedFoldedLines = ArrayUtil.EMPTY_INT_ARRAY;
496 int sum = 0;
497 for (int i = 0; i < length; i++) {
498 FoldRegion region = myCachedTopLevelRegions[i];
499 myCachedStartOffsets[i] = region.getStartOffset();
500 myCachedEndOffsets[i] = region.getEndOffset() - 1;
501 sum += region.getDocument().getLineNumber(region.getEndOffset()) - region.getDocument().getLineNumber(region.getStartOffset());
502 myCachedFoldedLines[i] = sum;
507 boolean addRegion(FoldRegion range) {
508 // During batchProcessing elements are inserted in ascending order,
509 // binary search find acceptable insertion place first time
510 int fastIndex = myCachedLastIndex != -1 && myIsBatchFoldingProcessing? myCachedLastIndex + 1:Collections.binarySearch(myRegions, range, OUR_COMPARATOR);
511 if (fastIndex < 0) fastIndex = -fastIndex - 1;
513 for (int i = fastIndex - 1; i >=0; --i) {
514 final FoldRegion region = myRegions.get(i);
515 if (region.getEndOffset() < range.getStartOffset()) break;
516 if (region.isValid() && intersects(region, range)) {
517 return false;
521 for (int i = fastIndex; i < myRegions.size(); i++) {
522 final FoldRegion region = myRegions.get(i);
524 if (range.getStartOffset() < region.getStartOffset() ||
525 range.getStartOffset() == region.getStartOffset() && range.getEndOffset() > region.getEndOffset()) {
526 for (int j = i + 1; j < myRegions.size(); j++) {
527 final FoldRegion next = myRegions.get(j);
528 if (next.getEndOffset() >= range.getEndOffset() && next.isValid()) {
529 if (next.getStartOffset() < range.getStartOffset()) {
530 return false;
532 else {
533 break;
538 myRegions.add(myCachedLastIndex = i, range);
539 return true;
542 myRegions.add(myCachedLastIndex = myRegions.size(),range);
543 return true;
546 FoldRegion fetchOutermost(int offset) {
547 if (!isFoldingEnabled()) return null;
549 final int[] starts = myCachedStartOffsets;
550 final int[] ends = myCachedEndOffsets;
552 int start = 0;
553 int end = ends.length - 1;
555 while (start <= end) {
556 int i = (start + end) / 2;
557 if (offset < starts[i]) {
558 end = i - 1;
559 } else if (offset > ends[i]) {
560 start = i + 1;
562 else {
563 return myCachedTopLevelRegions[i];
567 return null;
570 FoldRegion[] fetchVisible() {
571 if (!isFoldingEnabled()) return FoldRegion.EMPTY_ARRAY;
572 return myCachedVisible;
575 FoldRegion[] fetchTopLevel() {
576 if (!isFoldingEnabled()) return null;
577 return myCachedTopLevelRegions;
580 private boolean contains(FoldRegion outer, FoldRegion inner) {
581 return outer.getStartOffset() < inner.getStartOffset() && outer.getEndOffset() > inner.getStartOffset();
584 private boolean intersects(FoldRegion r1, FoldRegion r2) {
585 final int s1 = r1.getStartOffset();
586 final int s2 = r2.getStartOffset();
587 final int e1 = r1.getEndOffset();
588 final int e2 = r2.getEndOffset();
589 return s1 == s2 && e1 == e2 || s1 < s2 && s2 < e1 && e1 < e2 || s2 < s1 && s1 < e2 && e2 < e1;
592 private boolean contains(FoldRegion region, int offset) {
593 return region.getStartOffset() < offset && region.getEndOffset() > offset;
596 public FoldRegion[] fetchCollapsedAt(int offset) {
597 if (!isFoldingEnabled()) return FoldRegion.EMPTY_ARRAY;
598 ArrayList<FoldRegion> allCollapsed = new ArrayList<FoldRegion>();
599 for (FoldRegion region : myRegions) {
600 if (!region.isExpanded() && contains(region, offset)) {
601 allCollapsed.add(region);
605 return allCollapsed.toArray(new FoldRegion[allCollapsed.size()]);
608 boolean intersectsRegion(int startOffset, int endOffset) {
609 if (!FoldingModelImpl.this.isFoldingEnabled()) return true;
610 for (FoldRegion region : myRegions) {
611 boolean contains1 = contains(region, startOffset);
612 boolean contains2 = contains(region, endOffset);
613 if (contains1 != contains2) {
614 return true;
617 return false;
620 FoldRegion[] fetchAllRegions() {
621 if (!isFoldingEnabled()) return FoldRegion.EMPTY_ARRAY;
623 return myRegions.toArray(new FoldRegion[myRegions.size()]);
626 void removeRegion(FoldRegion range) {
627 myRegions.remove(range);
630 int getFoldedLinesCountBefore(int offset) {
631 int idx = getLastTopLevelIndexBefore(offset);
632 if (idx == -1) return 0;
633 return myCachedFoldedLines[idx];
636 public int getLastTopLevelIndexBefore(int offset) {
637 if (!isFoldingEnabled()) return -1;
639 int start = 0;
640 int end = myCachedEndOffsets.length - 1;
642 while (start <= end) {
643 int i = (start + end) / 2;
644 if (offset < myCachedEndOffsets[i]) {
645 end = i - 1;
646 } else if (offset > myCachedEndOffsets[i]) {
647 start = i + 1;
649 else {
650 return i;
654 return end;
656 // for (int i = 0; i < myCachedEndOffsets.length; i++) {
657 // if (!myCachedTopLevelRegions[i].isValid()) continue;
658 // int endOffset = myCachedEndOffsets[i];
659 // if (endOffset > offset) break;
660 // lastIndex = i;
661 // }
663 // return lastIndex;
666 public FoldRegion[] fetchAllRegionsIncludingInvalid() {
667 if (!FoldingModelImpl.this.isFoldingEnabled()) return FoldRegion.EMPTY_ARRAY;
669 return myRegions.toArray(new FoldRegion[myRegions.size()]);
673 public void beforeDocumentChange(DocumentEvent event) {
676 public void documentChanged(DocumentEvent event) {
677 if (((DocumentEx)event.getDocument()).isInBulkUpdate()) {
678 myFoldTree.clear();
679 } else {
680 updateCachedOffsets();
684 public int getPriority() {
685 return 1;