improve exception's description message
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / CaretModelImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 /*
18  * Created by IntelliJ IDEA.
19  * User: max
20  * Date: Jun 18, 2002
21  * Time: 9:12:05 PM
22  * To change template for new class use
23  * Code Style | Class Templates options (Tools | IDE Options).
24  */
25 package com.intellij.openapi.editor.impl;
26
27 import com.intellij.diagnostic.Dumpable;
28 import com.intellij.openapi.Disposable;
29 import com.intellij.openapi.application.ApplicationManager;
30 import com.intellij.openapi.editor.*;
31 import com.intellij.openapi.editor.colors.EditorColors;
32 import com.intellij.openapi.editor.event.CaretEvent;
33 import com.intellij.openapi.editor.event.CaretListener;
34 import com.intellij.openapi.editor.event.DocumentEvent;
35 import com.intellij.openapi.editor.ex.EditorEx;
36 import com.intellij.openapi.editor.ex.PrioritizedDocumentListener;
37 import com.intellij.openapi.editor.markup.TextAttributes;
38 import com.intellij.openapi.util.Disposer;
39 import com.intellij.util.EventDispatcher;
40 import com.intellij.util.containers.ContainerUtil;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import java.beans.PropertyChangeEvent;
45 import java.beans.PropertyChangeListener;
46 import java.util.*;
47
48 public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener, Disposable, Dumpable, InlayModel.Listener {
49   private final EditorImpl myEditor;
50   
51   private final EventDispatcher<CaretListener> myCaretListeners = EventDispatcher.create(CaretListener.class);
52
53   private TextAttributes myTextAttributes;
54
55   boolean myIsInUpdate;
56
57   final RangeMarkerTree<CaretImpl.PositionMarker> myPositionMarkerTree;
58   final RangeMarkerTree<CaretImpl.SelectionMarker> mySelectionMarkerTree;
59
60   private final LinkedList<CaretImpl> myCarets = new LinkedList<>();
61   private CaretImpl myCurrentCaret; // active caret in the context of 'runForEachCaret' call
62   private boolean myPerformCaretMergingAfterCurrentOperation;
63
64   int myDocumentUpdateCounter;
65
66   public CaretModelImpl(EditorImpl editor) {
67     myEditor = editor;
68     myEditor.addPropertyChangeListener(new PropertyChangeListener() {
69       @Override
70       public void propertyChange(PropertyChangeEvent evt) {
71         if (EditorEx.PROP_COLUMN_MODE.equals(evt.getPropertyName()) && !myEditor.isColumnMode()) {
72           for (CaretImpl caret : myCarets) {
73             caret.resetVirtualSelection();
74           }
75         }
76       }
77     }, this);
78
79     myPositionMarkerTree = new RangeMarkerTree<>(myEditor.getDocument());
80     mySelectionMarkerTree = new RangeMarkerTree<>(myEditor.getDocument());
81   }
82
83   void initCarets() {
84     myCarets.add(new CaretImpl(myEditor));
85   }
86
87   void onBulkDocumentUpdateStarted() {
88   }
89
90   void onBulkDocumentUpdateFinished() {
91     doWithCaretMerging(() -> {}); // do caret merging if it's not scheduled for later
92   }
93
94   @Override
95   public void documentChanged(final DocumentEvent e) {
96     myIsInUpdate = false;
97     myDocumentUpdateCounter++;
98     if (!myEditor.getDocument().isInBulkUpdate()) {
99       doWithCaretMerging(() -> {}); // do caret merging if it's not scheduled for later
100     }
101   }
102
103   @Override
104   public void beforeDocumentChange(DocumentEvent e) {
105     myIsInUpdate = true;
106     if (!myEditor.getDocument().isInBulkUpdate() && e.isWholeTextReplaced()) {
107       for (CaretImpl caret : myCarets) {
108         caret.updateCachedStateIfNeeded(); // logical position will be needed to restore caret position via diff
109       }
110     }
111   }
112
113   @Override
114   public int getPriority() {
115     return EditorDocumentPriorities.CARET_MODEL;
116   }
117
118   @Override
119   public void dispose() {
120     for (CaretImpl caret : myCarets) {
121       Disposer.dispose(caret);
122     }
123   }
124
125   public void updateVisualPosition() {
126     for (CaretImpl caret : myCarets) {
127       caret.updateVisualPosition();
128     }
129   }
130
131   @Override
132   public void moveCaretRelatively(final int columnShift, final int lineShift, final boolean withSelection, final boolean blockSelection, final boolean scrollToCaret) {
133     getCurrentCaret().moveCaretRelatively(columnShift, lineShift, withSelection, scrollToCaret);
134   }
135
136   @Override
137   public void moveToLogicalPosition(@NotNull LogicalPosition pos) {
138     getCurrentCaret().moveToLogicalPosition(pos);
139   }
140
141   @Override
142   public void moveToVisualPosition(@NotNull VisualPosition pos) {
143     getCurrentCaret().moveToVisualPosition(pos);
144   }
145
146   @Override
147   public void moveToOffset(int offset) {
148     getCurrentCaret().moveToOffset(offset);
149   }
150
151   @Override
152   public void moveToOffset(int offset, boolean locateBeforeSoftWrap) {
153     getCurrentCaret().moveToOffset(offset, locateBeforeSoftWrap);
154   }
155
156   @Override
157   public boolean isUpToDate() {
158     return getCurrentCaret().isUpToDate();
159   }
160
161   @NotNull
162   @Override
163   public LogicalPosition getLogicalPosition() {
164     return getCurrentCaret().getLogicalPosition();
165   }
166
167   @NotNull
168   @Override
169   public VisualPosition getVisualPosition() {
170     return getCurrentCaret().getVisualPosition();
171   }
172
173   @Override
174   public int getOffset() {
175     return getCurrentCaret().getOffset();
176   }
177
178   @Override
179   public int getVisualLineStart() {
180     return getCurrentCaret().getVisualLineStart();
181   }
182
183   @Override
184   public int getVisualLineEnd() {
185     return getCurrentCaret().getVisualLineEnd();
186   }
187
188   int getWordAtCaretStart() {
189     return getCurrentCaret().getWordAtCaretStart();
190   }
191
192   int getWordAtCaretEnd() {
193     return getCurrentCaret().getWordAtCaretEnd();
194   }
195
196   @Override
197   public void addCaretListener(@NotNull final CaretListener listener) {
198     myCaretListeners.addListener(listener);
199   }
200
201   @Override
202   public void removeCaretListener(@NotNull CaretListener listener) {
203     myCaretListeners.removeListener(listener);
204   }
205
206   @Override
207   public TextAttributes getTextAttributes() {
208     if (myTextAttributes == null) {
209       myTextAttributes = new TextAttributes();
210       if (myEditor.getSettings().isCaretRowShown()) {
211         myTextAttributes.setBackgroundColor(myEditor.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR));
212       }
213     }
214
215     return myTextAttributes;
216   }
217
218   public void reinitSettings() {
219     myTextAttributes = null;
220   }
221
222   @Override
223   public boolean supportsMultipleCarets() {
224     return true;
225   }
226
227   @Override
228   @NotNull
229   public CaretImpl getCurrentCaret() {
230     CaretImpl currentCaret = myCurrentCaret;
231     return ApplicationManager.getApplication().isDispatchThread() && currentCaret != null ? currentCaret : getPrimaryCaret();
232   }
233
234   @Override
235   @NotNull
236   public CaretImpl getPrimaryCaret() {
237     synchronized (myCarets) {
238       return myCarets.get(myCarets.size() - 1);
239     }
240   }
241
242   @Override
243   public int getCaretCount() {
244     synchronized (myCarets) {
245       return myCarets.size();
246     }
247   }
248
249   @Override
250   @NotNull
251   public List<Caret> getAllCarets() {
252     List<Caret> carets;
253     synchronized (myCarets) {
254       carets = new ArrayList<>(myCarets);
255     }
256     Collections.sort(carets, CaretPositionComparator.INSTANCE);
257     return carets;
258   }
259
260   @Nullable
261   @Override
262   public Caret getCaretAt(@NotNull VisualPosition pos) {
263     synchronized (myCarets) {
264       for (CaretImpl caret : myCarets) {
265         if (caret.getVisualPosition().equals(pos)) {
266           return caret;
267         }
268       }
269       return null;
270     }
271   }
272
273   @Nullable
274   @Override
275   public Caret addCaret(@NotNull VisualPosition pos) {
276     return addCaret(pos, true);
277   }
278
279   @Nullable
280   @Override
281   public Caret addCaret(@NotNull VisualPosition pos, boolean makePrimary) {
282     EditorImpl.assertIsDispatchThread();
283     CaretImpl caret = new CaretImpl(myEditor);
284     caret.moveToVisualPosition(pos, false);
285     if (addCaret(caret, makePrimary)) {
286       return caret;
287     }
288     else {
289       Disposer.dispose(caret);
290       return null;
291     }
292   }
293
294   boolean addCaret(@NotNull CaretImpl caretToAdd, boolean makePrimary) {
295     for (CaretImpl caret : myCarets) {
296       if (caretsOverlap(caret, caretToAdd)) {
297         return false;
298       }
299     }
300     synchronized (myCarets) {
301       if (makePrimary) {
302         myCarets.addLast(caretToAdd);
303       }
304       else {
305         myCarets.addFirst(caretToAdd);
306       }
307     }
308     fireCaretAdded(caretToAdd);
309     return true;
310   }
311
312   @Override
313   public boolean removeCaret(@NotNull Caret caret) {
314     EditorImpl.assertIsDispatchThread();
315     if (myCarets.size() <= 1 || !(caret instanceof CaretImpl)) {
316       return false;
317     }
318     synchronized (myCarets) {
319       if (!myCarets.remove(caret)) {
320         return false;
321       }
322     }
323     fireCaretRemoved(caret);
324     Disposer.dispose(caret);
325     return true;
326   }
327
328   @Override
329   public void removeSecondaryCarets() {
330     EditorImpl.assertIsDispatchThread();
331     ListIterator<CaretImpl> caretIterator = myCarets.listIterator(myCarets.size() - 1);
332     while (caretIterator.hasPrevious()) {
333       CaretImpl caret = caretIterator.previous();
334       synchronized (myCarets) {
335         caretIterator.remove();
336       }
337       fireCaretRemoved(caret);
338       Disposer.dispose(caret);
339     }
340   }
341
342   @Override
343   public void runForEachCaret(@NotNull final CaretAction action) {
344     runForEachCaret(action, false);
345   }
346
347   @Override
348   public void runForEachCaret(@NotNull final CaretAction action, final boolean reverseOrder) {
349     EditorImpl.assertIsDispatchThread();
350     if (myCurrentCaret != null) {
351       throw new IllegalStateException("Recursive runForEachCaret invocations are not allowed");
352     }
353     doWithCaretMerging(() -> {
354       try {
355         List<Caret> sortedCarets = getAllCarets();
356         if (reverseOrder) {
357           Collections.reverse(sortedCarets);
358         }
359         for (Caret caret : sortedCarets) {
360           myCurrentCaret = (CaretImpl)caret;
361           action.perform(caret);
362         }
363       }
364       finally {
365         myCurrentCaret = null;
366       }
367     });
368   }
369
370   @Override
371   public void runBatchCaretOperation(@NotNull Runnable runnable) {
372     EditorImpl.assertIsDispatchThread();
373     doWithCaretMerging(runnable);
374   }
375
376   private void mergeOverlappingCaretsAndSelections() {
377     if (myCarets.size() <= 1) {
378       return;
379     }
380     LinkedList<CaretImpl> carets = new LinkedList<>(myCarets);
381     Collections.sort(carets, CaretPositionComparator.INSTANCE);
382     ListIterator<CaretImpl> it = carets.listIterator();
383     CaretImpl keepPrimary = getPrimaryCaret();
384     while (it.hasNext()) {
385       CaretImpl prevCaret = null;
386       if (it.hasPrevious()) {
387         prevCaret = it.previous();
388         it.next();
389       }
390       CaretImpl currCaret = it.next();
391       if (prevCaret != null && caretsOverlap(currCaret, prevCaret)) {
392         int newSelectionStart = Math.min(currCaret.getSelectionStart(), prevCaret.getSelectionStart());
393         int newSelectionEnd = Math.max(currCaret.getSelectionEnd(), prevCaret.getSelectionEnd());
394         CaretImpl toRetain, toRemove;
395         if (currCaret.getOffset() >= prevCaret.getSelectionStart() && currCaret.getOffset() <= prevCaret.getSelectionEnd()) {
396           toRetain = prevCaret;
397           toRemove = currCaret;
398           it.remove();
399           it.previous();
400         }
401         else {
402           toRetain = currCaret;
403           toRemove = prevCaret;
404           it.previous();
405           it.previous();
406           it.remove();
407         }
408         if (toRemove == keepPrimary) {
409           keepPrimary = toRetain;
410         }
411         removeCaret(toRemove);
412         if (newSelectionStart < newSelectionEnd) {
413           toRetain.setSelection(newSelectionStart, newSelectionEnd);
414         }
415       }
416     }
417     if (keepPrimary != getPrimaryCaret()) {
418       synchronized (myCarets) {
419         myCarets.remove(keepPrimary);
420         myCarets.add(keepPrimary);
421       }
422     }
423   }
424
425   private static boolean caretsOverlap(@NotNull CaretImpl firstCaret, @NotNull CaretImpl secondCaret) {
426     if (firstCaret.getVisualPosition().equals(secondCaret.getVisualPosition())) {
427       return true;
428     }
429     int firstStart = firstCaret.getSelectionStart();
430     int secondStart = secondCaret.getSelectionStart();
431     int firstEnd = firstCaret.getSelectionEnd();
432     int secondEnd = secondCaret.getSelectionEnd();
433     return firstStart < secondStart && firstEnd > secondStart
434       || firstStart > secondStart && firstStart < secondEnd
435       || firstStart == secondStart && secondEnd != secondStart && firstEnd > firstStart
436       || (hasPureVirtualSelection(firstCaret) || hasPureVirtualSelection(secondCaret)) && (firstStart == secondStart || firstEnd == secondEnd);
437   }
438
439   private static boolean hasPureVirtualSelection(CaretImpl firstCaret) {
440     return firstCaret.getSelectionStart() == firstCaret.getSelectionEnd() && firstCaret.hasVirtualSelection();
441   }
442
443   void doWithCaretMerging(Runnable runnable) {
444     if (myPerformCaretMergingAfterCurrentOperation) {
445       runnable.run();
446     }
447     else {
448       myPerformCaretMergingAfterCurrentOperation = true;
449       try {
450         runnable.run();
451         mergeOverlappingCaretsAndSelections();
452       }
453       finally {
454         myPerformCaretMergingAfterCurrentOperation = false;
455       }
456     }
457   }
458
459   @Override
460   public void setCaretsAndSelections(@NotNull final List<CaretState> caretStates) {
461     setCaretsAndSelections(caretStates, true);
462   }
463
464   @Override
465   public void setCaretsAndSelections(@NotNull final List<CaretState> caretStates, final boolean updateSystemSelection) {
466     EditorImpl.assertIsDispatchThread();
467     if (caretStates.isEmpty()) {
468       throw new IllegalArgumentException("At least one caret should exist");
469     }
470     doWithCaretMerging(() -> {
471       int index = 0;
472       int oldCaretCount = myCarets.size();
473       Iterator<CaretImpl> caretIterator = myCarets.iterator();
474       for (CaretState caretState : caretStates) {
475         CaretImpl caret;
476         boolean caretAdded;
477         if (index++ < oldCaretCount) {
478           caret = caretIterator.next();
479           caretAdded = false;
480         }
481         else {
482           caret = new CaretImpl(myEditor);
483           if (caretState != null && caretState.getCaretPosition() != null) {
484             caret.moveToLogicalPosition(caretState.getCaretPosition(), false, null, false);
485           }
486           synchronized (myCarets) {
487             myCarets.add(caret);
488           }
489           fireCaretAdded(caret);
490           caretAdded = true;
491         }
492         if (caretState != null && caretState.getCaretPosition() != null && !caretAdded) {
493           caret.moveToLogicalPosition(caretState.getCaretPosition());
494         }
495         if (caretState != null && caretState.getSelectionStart() != null && caretState.getSelectionEnd() != null) {
496           caret.setSelection(myEditor.logicalToVisualPosition(caretState.getSelectionStart()),
497                              myEditor.logicalPositionToOffset(caretState.getSelectionStart()),
498                              myEditor.logicalToVisualPosition(caretState.getSelectionEnd()),
499                              myEditor.logicalPositionToOffset(caretState.getSelectionEnd()),
500                              updateSystemSelection);
501         }
502       }
503       int caretsToRemove = myCarets.size() - caretStates.size();
504       for (int i = 0; i < caretsToRemove; i++) {
505         CaretImpl caret;
506         synchronized (myCarets) {
507           caret = myCarets.removeLast();
508         }
509         fireCaretRemoved(caret);
510         Disposer.dispose(caret);
511       }
512     });
513   }
514
515   @NotNull
516   @Override
517   public List<CaretState> getCaretsAndSelections() {
518     synchronized (myCarets) {
519       List<CaretState> states = new ArrayList<>(myCarets.size());
520       for (CaretImpl caret : myCarets) {
521         states.add(new CaretState(caret.getLogicalPosition(),
522                                   caret.getSelectionStartLogicalPosition(),
523                                   caret.getSelectionEndLogicalPosition()));
524       }
525       return states;
526     }
527   }
528
529   void fireCaretPositionChanged(CaretEvent caretEvent) {
530     myCaretListeners.getMulticaster().caretPositionChanged(caretEvent);
531   }
532
533   void fireCaretAdded(@NotNull Caret caret) {
534     myCaretListeners.getMulticaster().caretAdded(new CaretEvent(myEditor, caret, caret.getLogicalPosition(), caret.getLogicalPosition()));
535   }
536
537   void fireCaretRemoved(@NotNull Caret caret) {
538     myCaretListeners.getMulticaster().caretRemoved(new CaretEvent(myEditor, caret, caret.getLogicalPosition(), caret.getLogicalPosition()));
539   }
540
541   public boolean isIteratingOverCarets() {
542     return myCurrentCaret != null;
543   }
544
545   @NotNull
546   @Override
547   public String dumpState() {
548     return "[in update: " + myIsInUpdate +
549            ", update counter: " + myDocumentUpdateCounter +
550            ", perform caret merging: " + myPerformCaretMergingAfterCurrentOperation +
551            ", current caret: " + myCurrentCaret +
552            ", all carets: " + ContainerUtil.map(myCarets, CaretImpl::dumpState) + "]";
553   }
554
555   @Override
556   public void onAdded(@NotNull Inlay inlay) {
557     if (myEditor.getDocument().isInBulkUpdate()) return;
558     int offset = inlay.getOffset();
559     for (CaretImpl caret : myCarets) {
560       caret.onInlayAdded(offset);
561     }
562   }
563
564   @Override
565   public void onRemoved(@NotNull Inlay inlay) {
566     if (myEditor.getDocument().isInEventsHandling() || myEditor.getDocument().isInBulkUpdate()) return;
567     doWithCaretMerging(this::updateVisualPosition);
568   }
569
570   @Override
571   public void onUpdated(@NotNull Inlay inlay) {
572     if (myEditor.getDocument().isInBulkUpdate()) return;
573     updateVisualPosition();
574   }
575
576   private static class VisualPositionComparator implements Comparator<VisualPosition> {
577     private static final VisualPositionComparator INSTANCE = new VisualPositionComparator();
578
579     @Override
580     public int compare(VisualPosition o1, VisualPosition o2) {
581       if (o1.line != o2.line) {
582         return o1.line - o2.line;
583       }
584       return o1.column - o2.column;
585     }
586   }
587
588   private static class CaretPositionComparator implements Comparator<Caret> {
589     private static final CaretPositionComparator INSTANCE = new CaretPositionComparator();
590
591     @Override
592     public int compare(Caret o1, Caret o2) {
593       return VisualPositionComparator.INSTANCE.compare(o1.getVisualPosition(), o2.getVisualPosition());
594     }
595   }
596 }