optimization: replace all occurrence is dramatically faster
[fedora-idea.git] / platform-impl / src / com / intellij / openapi / editor / impl / FoldingModelImpl.java
blobd5c83ffdae91969ec164663229359950851e0ca1
1 /*
2 * Created by IntelliJ IDEA.
3 * User: max
4 * Date: Jun 4, 2002
5 * Time: 8:27:13 PM
6 * To change template for new class use
7 * Code Style | Class Templates options (Tools | IDE Options).
8 */
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;
28 import java.awt.*;
29 import java.util.*;
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) {
54 myEditor = editor;
55 myIsFoldingEnabled = true;
56 myIsBatchFoldingProcessing = false;
57 myDoNotCollapseCaret = false;
58 myFoldTree = new FoldRegionsTree();
59 myFoldRegionsProcessed = false;
60 refreshSettings();
63 @NotNull
64 public List<FoldRegion> getGroupedRegions(@NotNull FoldingGroup group) {
65 return (List<FoldRegion>)myGroups.get(group);
68 @NotNull
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()) {
75 main = region;
78 return main;
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());
87 return endOffset;
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.");
122 return false;
125 myFoldRegionsProcessed = true;
126 if (myFoldTree.addRegion(region)) {
127 final FoldingGroup group = region.getGroup();
128 if (group != null) {
129 myGroups.putValue(group, region);
131 return true;
135 return false;
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;
147 if (!oldBatchFlag) {
148 mySavedCaretShift = myEditor.visibleLineNumberToYPosition(myEditor.getCaretModel().getVisualPosition().line) - myEditor.getScrollingModel().getVerticalScrollOffset();
151 myIsBatchFoldingProcessing = true;
152 myFoldTree.myCachedLastIndex = -1;
153 operation.run();
154 myFoldTree.myCachedLastIndex = -1;
156 if (!oldBatchFlag) {
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;
174 @NotNull
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);
191 int line = pos.line;
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();
217 if (group != null) {
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();
292 int column = -1;
293 int line = -1;
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;
310 if (column != -1) {
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);
315 } else {
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;
399 void rebuild() {
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()) {
406 visible.add(region);
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;
424 return 0;
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);
433 break;
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;
446 return 0;
450 updateCachedOffsets();
453 void updateCachedOffsets() {
454 if (FoldingModelImpl.this.isFoldingEnabled()) {
455 if (myCachedVisible == null) {
456 rebuild();
457 return;
460 for (FoldRegion foldRegion : myCachedVisible) {
461 if (!foldRegion.isValid()) {
462 rebuild();
463 return;
467 int length = myCachedTopLevelRegions.length;
468 if (myCachedEndOffsets == null || myCachedEndOffsets.length != length) {
469 if (length != 0) {
470 myCachedEndOffsets = new int[length];
471 myCachedStartOffsets = new int[length];
472 myCachedFoldedLines = new int[length];
474 else {
475 myCachedEndOffsets = ArrayUtil.EMPTY_INT_ARRAY;
476 myCachedStartOffsets = ArrayUtil.EMPTY_INT_ARRAY;
477 myCachedFoldedLines = ArrayUtil.EMPTY_INT_ARRAY;
481 int sum = 0;
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)) {
502 return false;
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()) {
515 return false;
517 else {
518 break;
523 myRegions.add(myCachedLastIndex = i, range);
524 return true;
527 myRegions.add(myCachedLastIndex = myRegions.size(),range);
528 return true;
531 FoldRegion fetchOutermost(int offset) {
532 if (!isFoldingEnabled()) return null;
534 final int[] starts = myCachedStartOffsets;
535 final int[] ends = myCachedEndOffsets;
537 int start = 0;
538 int end = ends.length - 1;
540 while (start <= end) {
541 int i = (start + end) / 2;
542 if (offset < starts[i]) {
543 end = i - 1;
544 } else if (offset > ends[i]) {
545 start = i + 1;
547 else {
548 return myCachedTopLevelRegions[i];
552 return null;
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) {
601 return true;
604 return false;
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;
626 int start = 0;
627 int end = myCachedEndOffsets.length - 1;
629 while (start <= end) {
630 int i = (start + end) / 2;
631 if (offset < myCachedEndOffsets[i]) {
632 end = i - 1;
633 } else if (offset > myCachedEndOffsets[i]) {
634 start = i + 1;
636 else {
637 return i;
641 return end;
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;
647 // lastIndex = i;
648 // }
650 // return lastIndex;
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()) {
665 myFoldTree.clear();
666 } else {
667 updateCachedOffsets();
671 public int getPriority() {
672 return 1;