2 * Copyright 2000-2015 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.
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.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;
44 import java.beans.PropertyChangeEvent;
45 import java.beans.PropertyChangeListener;
48 public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener, Disposable, Dumpable, InlayModel.Listener {
49 private final EditorImpl myEditor;
51 private final EventDispatcher<CaretListener> myCaretListeners = EventDispatcher.create(CaretListener.class);
53 private TextAttributes myTextAttributes;
57 final RangeMarkerTree<CaretImpl.PositionMarker> myPositionMarkerTree;
58 final RangeMarkerTree<CaretImpl.SelectionMarker> mySelectionMarkerTree;
60 private final LinkedList<CaretImpl> myCarets = new LinkedList<>();
61 private CaretImpl myCurrentCaret; // active caret in the context of 'runForEachCaret' call
62 private boolean myPerformCaretMergingAfterCurrentOperation;
64 int myDocumentUpdateCounter;
66 public CaretModelImpl(EditorImpl editor) {
68 myEditor.addPropertyChangeListener(new PropertyChangeListener() {
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();
79 myPositionMarkerTree = new RangeMarkerTree<>(myEditor.getDocument());
80 mySelectionMarkerTree = new RangeMarkerTree<>(myEditor.getDocument());
84 myCarets.add(new CaretImpl(myEditor));
87 void onBulkDocumentUpdateStarted() {
90 void onBulkDocumentUpdateFinished() {
91 doWithCaretMerging(() -> {}); // do caret merging if it's not scheduled for later
95 public void documentChanged(final DocumentEvent e) {
97 myDocumentUpdateCounter++;
98 if (!myEditor.getDocument().isInBulkUpdate()) {
99 doWithCaretMerging(() -> {}); // do caret merging if it's not scheduled for later
104 public void beforeDocumentChange(DocumentEvent e) {
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
114 public int getPriority() {
115 return EditorDocumentPriorities.CARET_MODEL;
119 public void dispose() {
120 for (CaretImpl caret : myCarets) {
121 Disposer.dispose(caret);
125 public void updateVisualPosition() {
126 for (CaretImpl caret : myCarets) {
127 caret.updateVisualPosition();
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);
137 public void moveToLogicalPosition(@NotNull LogicalPosition pos) {
138 getCurrentCaret().moveToLogicalPosition(pos);
142 public void moveToVisualPosition(@NotNull VisualPosition pos) {
143 getCurrentCaret().moveToVisualPosition(pos);
147 public void moveToOffset(int offset) {
148 getCurrentCaret().moveToOffset(offset);
152 public void moveToOffset(int offset, boolean locateBeforeSoftWrap) {
153 getCurrentCaret().moveToOffset(offset, locateBeforeSoftWrap);
157 public boolean isUpToDate() {
158 return getCurrentCaret().isUpToDate();
163 public LogicalPosition getLogicalPosition() {
164 return getCurrentCaret().getLogicalPosition();
169 public VisualPosition getVisualPosition() {
170 return getCurrentCaret().getVisualPosition();
174 public int getOffset() {
175 return getCurrentCaret().getOffset();
179 public int getVisualLineStart() {
180 return getCurrentCaret().getVisualLineStart();
184 public int getVisualLineEnd() {
185 return getCurrentCaret().getVisualLineEnd();
188 int getWordAtCaretStart() {
189 return getCurrentCaret().getWordAtCaretStart();
192 int getWordAtCaretEnd() {
193 return getCurrentCaret().getWordAtCaretEnd();
197 public void addCaretListener(@NotNull final CaretListener listener) {
198 myCaretListeners.addListener(listener);
202 public void removeCaretListener(@NotNull CaretListener listener) {
203 myCaretListeners.removeListener(listener);
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));
215 return myTextAttributes;
218 public void reinitSettings() {
219 myTextAttributes = null;
223 public boolean supportsMultipleCarets() {
229 public CaretImpl getCurrentCaret() {
230 CaretImpl currentCaret = myCurrentCaret;
231 return ApplicationManager.getApplication().isDispatchThread() && currentCaret != null ? currentCaret : getPrimaryCaret();
236 public CaretImpl getPrimaryCaret() {
237 synchronized (myCarets) {
238 return myCarets.get(myCarets.size() - 1);
243 public int getCaretCount() {
244 synchronized (myCarets) {
245 return myCarets.size();
251 public List<Caret> getAllCarets() {
253 synchronized (myCarets) {
254 carets = new ArrayList<>(myCarets);
256 Collections.sort(carets, CaretPositionComparator.INSTANCE);
262 public Caret getCaretAt(@NotNull VisualPosition pos) {
263 synchronized (myCarets) {
264 for (CaretImpl caret : myCarets) {
265 if (caret.getVisualPosition().equals(pos)) {
275 public Caret addCaret(@NotNull VisualPosition pos) {
276 return addCaret(pos, true);
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)) {
289 Disposer.dispose(caret);
294 boolean addCaret(@NotNull CaretImpl caretToAdd, boolean makePrimary) {
295 for (CaretImpl caret : myCarets) {
296 if (caretsOverlap(caret, caretToAdd)) {
300 synchronized (myCarets) {
302 myCarets.addLast(caretToAdd);
305 myCarets.addFirst(caretToAdd);
308 fireCaretAdded(caretToAdd);
313 public boolean removeCaret(@NotNull Caret caret) {
314 EditorImpl.assertIsDispatchThread();
315 if (myCarets.size() <= 1 || !(caret instanceof CaretImpl)) {
318 synchronized (myCarets) {
319 if (!myCarets.remove(caret)) {
323 fireCaretRemoved(caret);
324 Disposer.dispose(caret);
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();
337 fireCaretRemoved(caret);
338 Disposer.dispose(caret);
343 public void runForEachCaret(@NotNull final CaretAction action) {
344 runForEachCaret(action, false);
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");
353 doWithCaretMerging(() -> {
355 List<Caret> sortedCarets = getAllCarets();
357 Collections.reverse(sortedCarets);
359 for (Caret caret : sortedCarets) {
360 myCurrentCaret = (CaretImpl)caret;
361 action.perform(caret);
365 myCurrentCaret = null;
371 public void runBatchCaretOperation(@NotNull Runnable runnable) {
372 EditorImpl.assertIsDispatchThread();
373 doWithCaretMerging(runnable);
376 private void mergeOverlappingCaretsAndSelections() {
377 if (myCarets.size() <= 1) {
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();
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;
402 toRetain = currCaret;
403 toRemove = prevCaret;
408 if (toRemove == keepPrimary) {
409 keepPrimary = toRetain;
411 removeCaret(toRemove);
412 if (newSelectionStart < newSelectionEnd) {
413 toRetain.setSelection(newSelectionStart, newSelectionEnd);
417 if (keepPrimary != getPrimaryCaret()) {
418 synchronized (myCarets) {
419 myCarets.remove(keepPrimary);
420 myCarets.add(keepPrimary);
425 private static boolean caretsOverlap(@NotNull CaretImpl firstCaret, @NotNull CaretImpl secondCaret) {
426 if (firstCaret.getVisualPosition().equals(secondCaret.getVisualPosition())) {
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);
439 private static boolean hasPureVirtualSelection(CaretImpl firstCaret) {
440 return firstCaret.getSelectionStart() == firstCaret.getSelectionEnd() && firstCaret.hasVirtualSelection();
443 void doWithCaretMerging(Runnable runnable) {
444 if (myPerformCaretMergingAfterCurrentOperation) {
448 myPerformCaretMergingAfterCurrentOperation = true;
451 mergeOverlappingCaretsAndSelections();
454 myPerformCaretMergingAfterCurrentOperation = false;
460 public void setCaretsAndSelections(@NotNull final List<CaretState> caretStates) {
461 setCaretsAndSelections(caretStates, true);
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");
470 doWithCaretMerging(() -> {
472 int oldCaretCount = myCarets.size();
473 Iterator<CaretImpl> caretIterator = myCarets.iterator();
474 for (CaretState caretState : caretStates) {
477 if (index++ < oldCaretCount) {
478 caret = caretIterator.next();
482 caret = new CaretImpl(myEditor);
483 if (caretState != null && caretState.getCaretPosition() != null) {
484 caret.moveToLogicalPosition(caretState.getCaretPosition(), false, null, false);
486 synchronized (myCarets) {
489 fireCaretAdded(caret);
492 if (caretState != null && caretState.getCaretPosition() != null && !caretAdded) {
493 caret.moveToLogicalPosition(caretState.getCaretPosition());
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);
503 int caretsToRemove = myCarets.size() - caretStates.size();
504 for (int i = 0; i < caretsToRemove; i++) {
506 synchronized (myCarets) {
507 caret = myCarets.removeLast();
509 fireCaretRemoved(caret);
510 Disposer.dispose(caret);
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()));
529 void fireCaretPositionChanged(CaretEvent caretEvent) {
530 myCaretListeners.getMulticaster().caretPositionChanged(caretEvent);
533 void fireCaretAdded(@NotNull Caret caret) {
534 myCaretListeners.getMulticaster().caretAdded(new CaretEvent(myEditor, caret, caret.getLogicalPosition(), caret.getLogicalPosition()));
537 void fireCaretRemoved(@NotNull Caret caret) {
538 myCaretListeners.getMulticaster().caretRemoved(new CaretEvent(myEditor, caret, caret.getLogicalPosition(), caret.getLogicalPosition()));
541 public boolean isIteratingOverCarets() {
542 return myCurrentCaret != null;
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) + "]";
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);
565 public void onRemoved(@NotNull Inlay inlay) {
566 if (myEditor.getDocument().isInEventsHandling() || myEditor.getDocument().isInBulkUpdate()) return;
567 doWithCaretMerging(this::updateVisualPosition);
571 public void onUpdated(@NotNull Inlay inlay) {
572 if (myEditor.getDocument().isInBulkUpdate()) return;
573 updateVisualPosition();
576 private static class VisualPositionComparator implements Comparator<VisualPosition> {
577 private static final VisualPositionComparator INSTANCE = new VisualPositionComparator();
580 public int compare(VisualPosition o1, VisualPosition o2) {
581 if (o1.line != o2.line) {
582 return o1.line - o2.line;
584 return o1.column - o2.column;
588 private static class CaretPositionComparator implements Comparator<Caret> {
589 private static final CaretPositionComparator INSTANCE = new CaretPositionComparator();
592 public int compare(Caret o1, Caret o2) {
593 return VisualPositionComparator.INSTANCE.compare(o1.getVisualPosition(), o2.getVisualPosition());