make Caret.getOffset() work correctly from a side thread (under read action)
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / CaretImpl.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 package com.intellij.openapi.editor.impl;
17
18 import com.intellij.diagnostic.Dumpable;
19 import com.intellij.diagnostic.LogMessageEx;
20 import com.intellij.openapi.actionSystem.IdeActions;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.*;
24 import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
25 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
26 import com.intellij.openapi.editor.actions.EditorActionUtil;
27 import com.intellij.openapi.editor.event.CaretEvent;
28 import com.intellij.openapi.editor.event.DocumentEvent;
29 import com.intellij.openapi.editor.ex.DocumentEx;
30 import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
31 import com.intellij.openapi.editor.ex.FoldingModelEx;
32 import com.intellij.openapi.editor.ex.util.EditorUtil;
33 import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
34 import com.intellij.openapi.editor.impl.softwrap.SoftWrapHelper;
35 import com.intellij.openapi.ide.CopyPasteManager;
36 import com.intellij.openapi.util.Disposer;
37 import com.intellij.openapi.util.UserDataHolderBase;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.util.DocumentUtil;
40 import com.intellij.util.diff.FilesTooBigForDiffException;
41 import com.intellij.util.text.CharArrayUtil;
42 import com.intellij.util.ui.EmptyClipboardOwner;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.awt.*;
48 import java.awt.datatransfer.Clipboard;
49 import java.awt.datatransfer.StringSelection;
50 import java.util.List;
51
52 public class CaretImpl extends UserDataHolderBase implements Caret, Dumpable {
53   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.CaretImpl");
54
55   private final EditorImpl myEditor;
56   private boolean isValid = true;
57
58   private LogicalPosition myLogicalCaret;
59   private VerticalInfo myCaretInfo;
60   private VisualPosition myVisibleCaret;
61   private volatile PositionMarker myPositionMarker;
62   private boolean myLeansTowardsLargerOffsets;
63   private int myVirtualSpaceOffset;
64   private int myVisualLineStart;
65   private int myVisualLineEnd;
66   private boolean mySkipChangeRequests;
67   /**
68    * Initial horizontal caret position during vertical navigation.
69    * Similar to {@link #myDesiredX}, but represents logical caret position (<code>getLogicalPosition().column</code>) rather than visual.
70    */
71   private int myLastColumnNumber = 0;
72   private int myDesiredSelectionStartColumn = -1;
73   private int myDesiredSelectionEndColumn = -1;
74   /**
75    * We check that caret is located at the target offset at the end of {@link #moveToOffset(int, boolean)} method. However,
76    * it's possible that the following situation occurs:
77    * <p/>
78    * <pre>
79    * <ol>
80    *   <li>Some client subscribes to caret change events;</li>
81    *   <li>{@link #moveToLogicalPosition(LogicalPosition)} is called;</li>
82    *   <li>Caret position is changed during {@link #moveToLogicalPosition(LogicalPosition)} processing;</li>
83    *   <li>The client receives caret position change event and adjusts the position;</li>
84    *   <li>{@link #moveToLogicalPosition(LogicalPosition)} processing is finished;</li>
85    *   <li>{@link #moveToLogicalPosition(LogicalPosition)} reports an error because the caret is not located at the target offset;</li>
86    * </ol>
87    * </pre>
88    * <p/>
89    * This field serves as a flag that reports unexpected caret position change requests nested from {@link #moveToOffset(int, boolean)}.
90    */
91   private boolean myReportCaretMoves;
92   /**
93    * This field holds initial horizontal caret position during vertical navigation. It's used to determine target position when
94    * moving to the new line. It is stored in pixels, not in columns, to account for non-monospaced fonts as well.
95    * <p/>
96    * Negative value means no coordinate should be preserved.
97    */
98   private int myDesiredX = -1;
99
100   private volatile SelectionMarker mySelectionMarker;
101   private volatile VisualPosition myRangeMarkerStartPosition;
102   private volatile VisualPosition myRangeMarkerEndPosition;
103   private volatile boolean myRangeMarkerEndPositionIsLead;
104   boolean myUnknownDirection;
105
106   private int myDocumentUpdateCounter;
107
108   CaretImpl(EditorImpl editor) {
109     myEditor = editor;
110
111     myLogicalCaret = new LogicalPosition(0, 0);
112     myVisibleCaret = new VisualPosition(0, 0);
113     myCaretInfo = new VerticalInfo(0, 0);
114     myPositionMarker = new PositionMarker(0);
115     myVisualLineStart = 0;
116     Document doc = myEditor.getDocument();
117     myVisualLineEnd = doc.getLineCount() > 1 ? doc.getLineStartOffset(1) : doc.getLineCount() == 0 ? 0 : doc.getLineEndOffset(0);
118     myDocumentUpdateCounter = editor.getCaretModel().myDocumentUpdateCounter;
119   }
120
121   @Override
122   public void moveToOffset(int offset) {
123     moveToOffset(offset, false);
124   }
125
126   @Override
127   public void moveToOffset(final int offset, final boolean locateBeforeSoftWrap) {
128     assertIsDispatchThread();
129     validateCallContext();
130     if (mySkipChangeRequests) {
131       return;
132     }
133     myEditor.getCaretModel().doWithCaretMerging(() -> {
134       final LogicalPosition logicalPosition = myEditor.offsetToLogicalPosition(offset);
135       CaretEvent event = moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, null, false);
136       final LogicalPosition positionByOffsetAfterMove = myEditor.offsetToLogicalPosition(getOffset());
137       if (!positionByOffsetAfterMove.equals(logicalPosition)) {
138         StringBuilder debugBuffer = new StringBuilder();
139         moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, debugBuffer, true);
140         int actualOffset = getOffset();
141         int textStart = Math.max(0, Math.min(offset, actualOffset) - 1);
142         final DocumentEx document = myEditor.getDocument();
143         int textEnd = Math.min(document.getTextLength() - 1, Math.max(offset, actualOffset) + 1);
144         CharSequence text = document.getCharsSequence().subSequence(textStart, textEnd);
145         int inverseOffset = myEditor.logicalPositionToOffset(logicalPosition);
146         LogMessageEx.error(
147           LOG, "caret moved to wrong offset. Please submit a dedicated ticket and attach current editor's text to it.",
148           "Requested: offset=" + offset + ", logical position='" + logicalPosition + "' but actual: offset=" +
149           actualOffset + ", logical position='" + myLogicalCaret + "' (" + positionByOffsetAfterMove + "). " + myEditor.dumpState() +
150           "\ninterested text [" + textStart + ";" + textEnd + "): '" + text + "'\n debug trace: " + debugBuffer +
151           "\nLogical position -> offset ('" + logicalPosition + "'->'" + inverseOffset + "')"
152         );
153       }
154       if (event != null) {
155         myEditor.getCaretModel().fireCaretPositionChanged(event);
156         EditorActionUtil.selectNonexpandableFold(myEditor);
157       }
158     });
159   }
160
161   @NotNull
162   @Override
163   public CaretModel getCaretModel() {
164     return myEditor.getCaretModel();
165   }
166
167   @Override
168   public boolean isValid() {
169     return isValid;
170   }
171
172   @Override
173   public void moveCaretRelatively(final int _columnShift, final int lineShift, final boolean withSelection, final boolean scrollToCaret) {
174     assertIsDispatchThread();
175     if (mySkipChangeRequests) {
176       return;
177     }
178     if (myReportCaretMoves) {
179       LogMessageEx.error(LOG, "Unexpected caret move request");
180     }
181     if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling()) {
182       CopyPasteManager.getInstance().stopKillRings();
183     }
184     myEditor.getCaretModel().doWithCaretMerging(() -> {
185       updateCachedStateIfNeeded();
186
187       int oldOffset = getOffset();
188       int columnShift = _columnShift;
189       if (withSelection && lineShift == 0) {
190         if (columnShift == -1) {
191           int column = myVisibleCaret.column - (hasSelection() && oldOffset == getSelectionEnd() ? 2 : 1);
192           if (column >= 0 && myEditor.getInlayModel().hasInlineElementAt(new VisualPosition(myVisibleCaret.line, column))) {
193             columnShift = -2;
194           }
195         }
196         else if (columnShift == 1) {
197           if (myEditor.getInlayModel().hasInlineElementAt(
198             new VisualPosition(myVisibleCaret.line, myVisibleCaret.column + (hasSelection() && oldOffset == getSelectionStart() ? 1 : 0)))) {
199             columnShift = 2;
200           }
201         }
202       }
203       final int leadSelectionOffset = getLeadSelectionOffset();
204       final VisualPosition leadSelectionPosition = getLeadSelectionPosition();
205       EditorSettings editorSettings = myEditor.getSettings();
206       VisualPosition visualCaret = getVisualPosition();
207
208       int lastColumnNumber = myLastColumnNumber;
209       int desiredX = myDesiredX;
210       if (columnShift == 0) {
211         if (myDesiredX < 0) {
212           desiredX = getCurrentX();
213         }
214       }
215       else {
216         myDesiredX = desiredX = -1;
217       }
218
219       int newLineNumber = visualCaret.line + lineShift;
220       int newColumnNumber = visualCaret.column + columnShift;
221       boolean newLeansRight = lineShift == 0 && columnShift != 0 ? columnShift < 0 : visualCaret.leansRight;
222
223       if (desiredX >= 0) {
224         newColumnNumber = myEditor.xyToVisualPosition(new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight())).column;
225       }
226
227       Document document = myEditor.getDocument();
228       if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == 1) {
229         int lastLine = document.getLineCount() - 1;
230         if (lastLine < 0) lastLine = 0;
231         if (newColumnNumber > EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber) &&
232             newLineNumber < myEditor.logicalToVisualPosition(new LogicalPosition(lastLine, 0)).line) {
233           newColumnNumber = 0;
234           newLineNumber++;
235         }
236       }
237       else if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == -1) {
238         if (newColumnNumber < 0 && newLineNumber > 0) {
239           newLineNumber--;
240           newColumnNumber = EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber);
241         }
242       }
243
244       if (newColumnNumber < 0) newColumnNumber = 0;
245
246       // There is a possible case that caret is located at the first line and user presses 'Shift+Up'. We want to select all text
247       // from the document start to the current caret position then. So, we have a dedicated flag for tracking that.
248       boolean selectToDocumentStart = false;
249       if (newLineNumber < 0) {
250         selectToDocumentStart = true;
251         newLineNumber = 0;
252
253         // We want to move caret to the first column if it's already located at the first line and 'Up' is pressed.
254         newColumnNumber = 0;
255       }
256
257       VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber);
258       if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
259         LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber, newLeansRight));
260         int offset = myEditor.logicalPositionToOffset(log);
261         if (offset >= document.getTextLength() && (!myEditor.myUseNewRendering || columnShift == 0)) {
262           int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength(), true, false).column;
263           // We want to move caret to the last column if if it's located at the last line and 'Down' is pressed.
264           if (lastOffsetColumn > newColumnNumber) {
265             newColumnNumber = lastOffsetColumn;
266             newLeansRight = true;
267           }
268         }
269         if (!editorSettings.isCaretInsideTabs()) {
270           CharSequence text = document.getCharsSequence();
271           if (offset >= 0 && offset < document.getTextLength()) {
272             if (text.charAt(offset) == '\t' && (columnShift <= 0 || offset == oldOffset)) {
273               if (columnShift <= 0) {
274                 newColumnNumber = myEditor.offsetToVisualPosition(offset, true, false).column;
275               }
276               else {
277                 SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset + 1);
278                 // There is a possible case that tabulation symbol is the last document symbol represented on a visual line before
279                 // soft wrap. We can't just use column from 'offset + 1' because it would point on a next visual line.
280                 if (softWrap == null) {
281                   newColumnNumber = myEditor.offsetToVisualPosition(offset + 1).column;
282                 }
283                 else {
284                   newColumnNumber = EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber);
285                 }
286               }
287             }
288           }
289         }
290       }
291
292       pos = new VisualPosition(newLineNumber, newColumnNumber, newLeansRight);
293       if (columnShift != 0 && lineShift == 0 && myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
294         LogicalPosition logical = myEditor.visualToLogicalPosition(pos);
295         int softWrapOffset = myEditor.logicalPositionToOffset(logical);
296         if (columnShift >= 0) {
297           moveToOffset(softWrapOffset);
298         }
299         else {
300           int line = myEditor.offsetToVisualLine(softWrapOffset - 1);
301           moveToVisualPosition(new VisualPosition(line, EditorUtil.getLastVisualLineColumnNumber(myEditor, line)));
302         }
303       }
304       else {
305         moveToVisualPosition(pos);
306         if (!editorSettings.isVirtualSpace() && columnShift == 0 && lastColumnNumber >=0) {
307           setLastColumnNumber(lastColumnNumber);
308         }
309       }
310
311       if (withSelection) {
312         if (selectToDocumentStart) {
313           setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(0), 0);
314         }
315         else if (pos.line >= myEditor.getVisibleLineCount()) {
316           int endOffset = document.getTextLength();
317           if (leadSelectionOffset < endOffset) {
318             setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(endOffset), endOffset);
319           }
320         }
321         else {
322           int selectionStartToUse = leadSelectionOffset;
323           VisualPosition selectionStartPositionToUse = leadSelectionPosition;
324           if (isUnknownDirection() || oldOffset > getSelectionStart() && oldOffset < getSelectionEnd()) {
325             if (getOffset() > leadSelectionOffset ^ getSelectionStart() < getSelectionEnd()) {
326               selectionStartToUse = getSelectionEnd();
327               selectionStartPositionToUse = getSelectionEndPosition();
328             }
329             else {
330               selectionStartToUse = getSelectionStart();
331               selectionStartPositionToUse = getSelectionStartPosition();
332             }
333           }
334           setSelection(selectionStartPositionToUse, selectionStartToUse, getVisualPosition(), getOffset());
335         }
336       }
337       else {
338         removeSelection();
339       }
340
341       if (scrollToCaret) {
342         myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
343       }
344
345       if (desiredX >= 0) {
346         myDesiredX = desiredX;
347       }
348
349       EditorActionUtil.selectNonexpandableFold(myEditor);
350     });
351   }
352
353   @Override
354   public void moveToLogicalPosition(@NotNull final LogicalPosition pos) {
355     myEditor.getCaretModel().doWithCaretMerging(() -> moveToLogicalPosition(pos, false, null, true));
356   }
357
358
359   private CaretEvent doMoveToLogicalPosition(@NotNull LogicalPosition pos,
360                                              boolean locateBeforeSoftWrap,
361                                              @NonNls @Nullable StringBuilder debugBuffer,
362                                              boolean fireListeners) {
363     assertIsDispatchThread();
364     updateCachedStateIfNeeded();
365     if (debugBuffer != null) {
366       debugBuffer.append("Start moveToLogicalPosition(). Locate before soft wrap: ").append(locateBeforeSoftWrap).append(", position: ")
367         .append(pos).append("\n");
368     }
369     myDesiredX = -1;
370     validateCallContext();
371     int column = pos.column;
372     int line = pos.line;
373     int softWrapLinesBefore = pos.softWrapLinesBeforeCurrentLogicalLine;
374     int softWrapLinesCurrent = pos.softWrapLinesOnCurrentLogicalLine;
375     int softWrapColumns = pos.softWrapColumnDiff;
376     boolean leansForward = pos.leansForward;
377     boolean leansRight = pos.visualPositionLeansRight;
378
379     Document doc = myEditor.getDocument();
380
381     int lineCount = doc.getLineCount();
382     if (lineCount == 0) {
383       if (debugBuffer != null) {
384         debugBuffer.append("Resetting target logical line to zero as the document is empty\n");
385       }
386       line = 0;
387     }
388     else if (line > lineCount - 1) {
389       if (debugBuffer != null) {
390         debugBuffer.append("Resetting target logical line (").append(line).append(") to ").append(lineCount - 1)
391           .append(" as it is greater than total document lines number\n");
392       }
393       line = lineCount - 1;
394       softWrapLinesBefore = 0;
395       softWrapLinesCurrent = 0;
396     }
397
398     EditorSettings editorSettings = myEditor.getSettings();
399
400     if (!editorSettings.isVirtualSpace() && line < lineCount) {
401       int lineEndOffset = doc.getLineEndOffset(line);
402       final LogicalPosition endLinePosition = myEditor.offsetToLogicalPosition(lineEndOffset);
403       int lineEndColumnNumber = endLinePosition.column;
404       if (column > lineEndColumnNumber) {
405         int oldColumn = column;
406         column = lineEndColumnNumber;
407         leansForward = true;
408         leansRight = true;
409         if (softWrapColumns != 0) {
410           softWrapColumns -= column - lineEndColumnNumber;
411         }
412         if (debugBuffer != null) {
413           debugBuffer.append("Resetting target logical column (").append(oldColumn).append(") to ").append(lineEndColumnNumber)
414             .append(" because caret is not allowed to be located after line end (offset: ").append(lineEndOffset).append(", ")
415             .append("logical position: ").append(endLinePosition).append("). Current soft wrap columns value: ").append(softWrapColumns)
416             .append("\n");
417         }
418       }
419     }
420
421     myEditor.getFoldingModel().flushCaretPosition(this);
422
423     VerticalInfo oldInfo = myCaretInfo;
424     LogicalPosition oldCaretPosition = myLogicalCaret;
425     VisualPosition oldVisualPosition = myVisibleCaret;
426
427     LogicalPosition logicalPositionToUse;
428     if (pos.visualPositionAware) {
429       logicalPositionToUse = new LogicalPosition(
430         line, column, softWrapLinesBefore, softWrapLinesCurrent, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff, 
431         leansForward, leansRight
432       );
433     }
434     else {
435       logicalPositionToUse = new LogicalPosition(line, column, leansForward);
436     }
437     final int offset = myEditor.logicalPositionToOffset(logicalPositionToUse);
438     if (debugBuffer != null) {
439       debugBuffer.append("Resulting logical position to use: ").append(logicalPositionToUse).append(". It's mapped to offset ").append(offset).append("\n");
440     }
441
442     FoldRegion collapsedAt = myEditor.getFoldingModel().getCollapsedRegionAtOffset(offset);
443
444     if (collapsedAt != null && offset > collapsedAt.getStartOffset()) {
445       if (debugBuffer != null) {
446         debugBuffer.append("Scheduling expansion of fold region ").append(collapsedAt).append("\n");
447       }
448       Runnable runnable = () -> {
449         FoldRegion[] allCollapsedAt = myEditor.getFoldingModel().fetchCollapsedAt(offset);
450         for (FoldRegion foldRange : allCollapsedAt) {
451           foldRange.setExpanded(true);
452         }
453       };
454
455       mySkipChangeRequests = true;
456       try {
457         myEditor.getFoldingModel().runBatchFoldingOperation(runnable, false);
458       }
459       finally {
460         mySkipChangeRequests = false;
461       }
462       logicalPositionToUse = logicalPositionToUse.visualPositionAware ? logicalPositionToUse.withoutVisualPositionInfo() : logicalPositionToUse;
463     }
464
465     setCurrentLogicalCaret(logicalPositionToUse);
466     setLastColumnNumber(myLogicalCaret.column);
467     myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
468     myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
469
470     updateOffsetsFromLogicalPosition();
471     int newOffset = getOffset();
472     if (debugBuffer != null) {
473       debugBuffer.append("Storing offset ").append(newOffset).append(" (mapped from logical position ").append(myLogicalCaret).append(")\n");
474     }
475
476     updateVisualLineInfo();
477
478     myEditor.updateCaretCursor();
479     requestRepaint(oldInfo);
480
481     if (locateBeforeSoftWrap && SoftWrapHelper.isCaretAfterSoftWrap(this)) {
482       int lineToUse = myVisibleCaret.line - 1;
483       if (lineToUse >= 0) {
484         final VisualPosition visualPosition = new VisualPosition(lineToUse, EditorUtil.getLastVisualLineColumnNumber(myEditor, lineToUse));
485         if (debugBuffer != null) {
486           debugBuffer.append("Adjusting caret position by moving it before soft wrap. Moving to visual position ").append(visualPosition).append("\n");
487         }
488         final LogicalPosition logicalPosition = myEditor.visualToLogicalPosition(visualPosition);
489         final int tmpOffset = myEditor.logicalPositionToOffset(logicalPosition);
490         if (tmpOffset == newOffset) {
491           boolean restore = myReportCaretMoves;
492           myReportCaretMoves = false;
493           try {
494             moveToVisualPosition(visualPosition);
495             return null;
496           }
497           finally {
498             myReportCaretMoves = restore;
499           }
500         }
501         else {
502           LogMessageEx.error(LOG, "Invalid editor dimension mapping", "Expected to map visual position '" +
503           visualPosition + "' to offset " + newOffset + " but got the following: -> logical position '" +
504           logicalPosition + "'; -> offset " + tmpOffset + ". State: " + myEditor.dumpState());
505         }
506       }
507     }
508
509     if (myEditor.myUseNewRendering ? !oldVisualPosition.equals(myVisibleCaret) : 
510         !oldCaretPosition.toVisualPosition().equals(myLogicalCaret.toVisualPosition())) {
511       CaretEvent event = new CaretEvent(myEditor, this, oldCaretPosition, myLogicalCaret);
512       if (fireListeners) {
513         myEditor.getCaretModel().fireCaretPositionChanged(event);
514       }
515       else {
516         return event;
517       }
518     }
519     return null;
520   }
521
522   private void updateOffsetsFromLogicalPosition() {
523     int offset = myEditor.logicalPositionToOffset(myLogicalCaret);
524     myPositionMarker = new PositionMarker(offset);
525     myLeansTowardsLargerOffsets = myLogicalCaret.leansForward;
526     myVirtualSpaceOffset = myLogicalCaret.column - myEditor.offsetToLogicalPosition(offset).column;
527   }
528
529   private void setLastColumnNumber(int lastColumnNumber) {
530     myLastColumnNumber = lastColumnNumber;
531   }
532
533   private void requestRepaint(VerticalInfo oldCaretInfo) {
534     int lineHeight = myEditor.getLineHeight();
535     Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
536     final EditorGutterComponentEx gutter = myEditor.getGutterComponentEx();
537     final EditorComponentImpl content = myEditor.getContentComponent();
538
539     int updateWidth = myEditor.getScrollPane().getHorizontalScrollBar().getValue() + visibleArea.width;
540     if (Math.abs(myCaretInfo.y - oldCaretInfo.y) <= 2 * lineHeight) {
541       int minY = Math.min(oldCaretInfo.y, myCaretInfo.y);
542       int maxY = Math.max(oldCaretInfo.y + oldCaretInfo.height, myCaretInfo.y + myCaretInfo.height);
543       content.repaintEditorComponent(0, minY, updateWidth, maxY - minY);
544       gutter.repaint(0, minY, gutter.getWidth(), maxY - minY);
545     }
546     else {
547       content.repaintEditorComponent(0, oldCaretInfo.y, updateWidth, oldCaretInfo.height + lineHeight);
548       gutter.repaint(0, oldCaretInfo.y, updateWidth, oldCaretInfo.height + lineHeight);
549       content.repaintEditorComponent(0, myCaretInfo.y, updateWidth, myCaretInfo.height + lineHeight);
550       gutter.repaint(0, myCaretInfo.y, updateWidth, myCaretInfo.height + lineHeight);
551     }
552   }
553
554   @Override
555   public void moveToVisualPosition(@NotNull final VisualPosition pos) {
556     myEditor.getCaretModel().doWithCaretMerging(() -> moveToVisualPosition(pos, true));
557   }
558
559   void moveToVisualPosition(@NotNull VisualPosition pos, boolean fireListeners) {
560     assertIsDispatchThread();
561     validateCallContext();
562     if (mySkipChangeRequests) {
563       return;
564     }
565     if (myReportCaretMoves) {
566       LogMessageEx.error(LOG, "Unexpected caret move request");
567     }
568     if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling() && !pos.equals(myVisibleCaret)) {
569       CopyPasteManager.getInstance().stopKillRings();
570     }
571     updateCachedStateIfNeeded();
572
573     myDesiredX = -1;
574     int column = pos.column;
575     int line = pos.line;
576     boolean leanRight = pos.leansRight;
577
578     int lastLine = myEditor.getVisibleLineCount() - 1;
579     if (lastLine <= 0) {
580       lastLine = 0;
581     }
582
583     if (line > lastLine) {
584       line = lastLine;
585     }
586
587     EditorSettings editorSettings = myEditor.getSettings();
588
589     if (!editorSettings.isVirtualSpace()) {
590       int lineEndColumn = EditorUtil.getLastVisualLineColumnNumber(myEditor, line);
591       if (column > lineEndColumn && (!myEditor.myUseNewRendering || !myEditor.getSoftWrapModel().isInsideSoftWrap(pos))) {
592         column = lineEndColumn;
593         leanRight = true;
594       }
595
596       if (!myEditor.myUseNewRendering && column < 0 && line > 0) {
597         line--;
598         column = EditorUtil.getLastVisualLineColumnNumber(myEditor, line);
599       }
600     }
601
602     myVisibleCaret = new VisualPosition(line, column, leanRight);
603
604     VerticalInfo oldInfo = myCaretInfo;
605     LogicalPosition oldPosition = myLogicalCaret;
606
607     setCurrentLogicalCaret(myEditor.visualToLogicalPosition(myVisibleCaret));
608     updateOffsetsFromLogicalPosition();
609
610     updateVisualLineInfo();
611
612     myEditor.getFoldingModel().flushCaretPosition(this);
613
614     setLastColumnNumber(myLogicalCaret.column);
615     myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
616     myEditor.updateCaretCursor();
617     requestRepaint(oldInfo);
618
619     if (fireListeners && !oldPosition.equals(myLogicalCaret)) {
620       CaretEvent event = new CaretEvent(myEditor, this, oldPosition, myLogicalCaret);
621       myEditor.getCaretModel().fireCaretPositionChanged(event);
622     }
623   }
624
625   @Nullable
626   CaretEvent moveToLogicalPosition(@NotNull LogicalPosition pos,
627                                            boolean locateBeforeSoftWrap,
628                                            @Nullable StringBuilder debugBuffer,
629                                            boolean fireListeners) {
630     if (mySkipChangeRequests) {
631       return null;
632     }
633     if (myReportCaretMoves) {
634       LogMessageEx.error(LOG, "Unexpected caret move request");
635     }
636     if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling() && !pos.equals(myLogicalCaret)) {
637       CopyPasteManager.getInstance().stopKillRings();
638     }
639
640     myReportCaretMoves = true;
641     try {
642       return doMoveToLogicalPosition(pos, locateBeforeSoftWrap, debugBuffer, fireListeners);
643     }
644     finally {
645       myReportCaretMoves = false;
646     }
647   }
648
649   private static void assertIsDispatchThread() {
650     EditorImpl.assertIsDispatchThread();
651   }
652
653   private void validateCallContext() {
654     LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread() || !myEditor.getCaretModel().myIsInUpdate,
655                    "Caret model is in its update process. All requests are illegal at this point.");
656   }
657
658   @Override
659   public void dispose() {
660     if (myPositionMarker != null) {
661       myPositionMarker = null;
662     }
663     if (mySelectionMarker != null) {
664       mySelectionMarker = null;
665     }
666     isValid = false;
667   }
668
669   @Override
670   public boolean isUpToDate() {
671     return !myEditor.getCaretModel().myIsInUpdate && !myReportCaretMoves;
672   }
673
674   @NotNull
675   @Override
676   public LogicalPosition getLogicalPosition() {
677     validateCallContext();
678     updateCachedStateIfNeeded();
679     return myLogicalCaret;
680   }
681
682   @NotNull
683   @Override
684   public VisualPosition getVisualPosition() {
685     validateCallContext();
686     updateCachedStateIfNeeded();
687     return myVisibleCaret;
688   }
689
690   @Override
691   public int getOffset() {
692     validateCallContext();
693     validateContext(false);
694     PositionMarker marker = myPositionMarker;
695     if (marker == null) return 0; // caret was disposed
696     assert marker.isValid();
697     return marker.getStartOffset();
698   }
699
700   @Override
701   public int getVisualLineStart() {
702     updateCachedStateIfNeeded();
703     return myVisualLineStart;
704   }
705
706   @Override
707   public int getVisualLineEnd() {
708     updateCachedStateIfNeeded();
709     return myVisualLineEnd;
710   }
711
712   @NotNull
713   private VerticalInfo createVerticalInfo(LogicalPosition position) {
714     Document document = myEditor.getDocument();
715     int logicalLine = position.line;
716     if (logicalLine >= document.getLineCount()) {
717       logicalLine = Math.max(0, document.getLineCount() - 1);
718     }
719     int startOffset = document.getLineStartOffset(logicalLine);
720     int endOffset = document.getLineEndOffset(logicalLine);
721
722     // There is a possible case that active logical line is represented on multiple lines due to soft wraps processing.
723     // We want to highlight those visual lines as 'active' then, so, we calculate 'y' position for the logical line start
724     // and height in accordance with the number of occupied visual lines.
725     int y;
726     if (myEditor.myUseNewRendering) {
727       int visualLine = myEditor.offsetToVisualLine(document.getLineStartOffset(logicalLine));
728       y = myEditor.visibleLineToY(visualLine);
729     }
730     else {
731       VisualPosition visualPosition = myEditor.offsetToVisualPosition(document.getLineStartOffset(logicalLine));
732       y = myEditor.visualPositionToXY(visualPosition).y;
733     }
734     int lineHeight = myEditor.getLineHeight();
735     int height = lineHeight;
736     List<? extends SoftWrap> softWraps = myEditor.getSoftWrapModel().getSoftWrapsForRange(startOffset, endOffset);
737     for (SoftWrap softWrap : softWraps) {
738       height += StringUtil.countNewLines(softWrap.getText()) * lineHeight;
739     }
740
741     return new VerticalInfo(y, height);
742   }
743
744   /**
745    * Recalculates caret visual position without changing its logical position (called when soft wraps are changing)
746    */
747   void updateVisualPosition() {
748     updateCachedStateIfNeeded();
749     VerticalInfo oldInfo = myCaretInfo;
750     LogicalPosition visUnawarePos = new LogicalPosition(myLogicalCaret.line, myLogicalCaret.column, myLogicalCaret.leansForward);
751     setCurrentLogicalCaret(visUnawarePos);
752     myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
753     updateVisualLineInfo();
754
755     myEditor.updateCaretCursor();
756     requestRepaint(oldInfo);
757   }
758
759   private void updateVisualLineInfo() {
760     myVisualLineStart = myEditor.logicalPositionToOffset(myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line, 0)));
761     myVisualLineEnd = myEditor.logicalPositionToOffset(myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line + 1, 0)));
762   }
763
764   void onInlayAdded(int offset) {
765     updateCachedStateIfNeeded();
766     int currentOffset = getOffset();
767     if (offset == currentOffset && myLogicalCaret.leansForward) {
768       VisualPosition pos = myEditor.offsetToVisualPosition(currentOffset, true, false);
769       moveToVisualPosition(pos);
770     }
771     else {
772       updateVisualPosition();
773     }
774   }
775
776   private void setCurrentLogicalCaret(@NotNull LogicalPosition position) {
777     myLogicalCaret = position;
778     myCaretInfo = createVerticalInfo(position);
779   }
780
781   int getWordAtCaretStart() {
782     Document document = myEditor.getDocument();
783     int offset = getOffset();
784     if (offset == 0) return 0;
785     int lineNumber = getLogicalPosition().line;
786     int newOffset = offset - 1;
787     int minOffset = lineNumber > 0 ? document.getLineEndOffset(lineNumber - 1) : 0;
788     boolean camel = myEditor.getSettings().isCamelWords();
789     for (; newOffset > minOffset; newOffset--) {
790       if (EditorActionUtil.isWordOrLexemeStart(myEditor, newOffset, camel)) break;
791     }
792
793     return newOffset;
794   }
795
796   int getWordAtCaretEnd() {
797     Document document = myEditor.getDocument();
798     int offset = getOffset();
799
800     if (offset >= document.getTextLength() - 1 || document.getLineCount() == 0) return offset;
801
802     int newOffset = offset + 1;
803
804     int lineNumber = getLogicalPosition().line;
805     int maxOffset = document.getLineEndOffset(lineNumber);
806     if (newOffset > maxOffset) {
807       if (lineNumber + 1 >= document.getLineCount()) return offset;
808       maxOffset = document.getLineEndOffset(lineNumber + 1);
809     }
810     boolean camel = myEditor.getSettings().isCamelWords();
811     for (; newOffset < maxOffset; newOffset++) {
812       if (EditorActionUtil.isWordOrLexemeEnd(myEditor, newOffset, camel)) break;
813     }
814
815     return newOffset;
816   }
817
818   private CaretImpl cloneWithoutSelection() {
819     updateCachedStateIfNeeded();
820     CaretImpl clone = new CaretImpl(myEditor);
821     clone.myLogicalCaret = myLogicalCaret;
822     clone.myCaretInfo = myCaretInfo;
823     clone.myVisibleCaret = myVisibleCaret;
824     clone.myPositionMarker = new PositionMarker(getOffset());
825     clone.myLeansTowardsLargerOffsets = myLeansTowardsLargerOffsets;
826     clone.myVirtualSpaceOffset = myVirtualSpaceOffset;
827     clone.myVisualLineStart = myVisualLineStart;
828     clone.myVisualLineEnd = myVisualLineEnd;
829     clone.mySkipChangeRequests = mySkipChangeRequests;
830     clone.myLastColumnNumber = myLastColumnNumber;
831     clone.myReportCaretMoves = myReportCaretMoves;
832     clone.myDesiredX = myDesiredX;
833     clone.myDesiredSelectionStartColumn = -1;
834     clone.myDesiredSelectionEndColumn = -1;
835     return clone;
836   }
837
838   @Nullable
839   @Override
840   public Caret clone(boolean above) {
841     assertIsDispatchThread();
842     int lineShift = above ? -1 : 1;
843     final CaretImpl clone = cloneWithoutSelection();
844     final int newSelectionStartOffset;
845     final int newSelectionEndOffset;
846     final int newSelectionStartColumn;
847     final int newSelectionEndColumn;
848     final VisualPosition newSelectionStartPosition;
849     final VisualPosition newSelectionEndPosition;
850     final boolean hasNewSelection;
851     if (hasSelection() || myDesiredSelectionStartColumn >=0 || myDesiredSelectionEndColumn >= 0) {
852       VisualPosition startPosition = getSelectionStartPosition();
853       VisualPosition endPosition = getSelectionEndPosition();
854       VisualPosition leadPosition = getLeadSelectionPosition();
855       boolean leadIsStart = leadPosition.equals(startPosition);
856       boolean leadIsEnd = leadPosition.equals(endPosition);
857       LogicalPosition selectionStart = myEditor.visualToLogicalPosition(leadIsStart || leadIsEnd ? leadPosition : startPosition);
858       LogicalPosition selectionEnd = myEditor.visualToLogicalPosition(leadIsEnd ? startPosition : endPosition);
859       newSelectionStartColumn = myDesiredSelectionStartColumn < 0 ? selectionStart.column : myDesiredSelectionStartColumn;
860       newSelectionEndColumn = myDesiredSelectionEndColumn < 0 ? selectionEnd.column : myDesiredSelectionEndColumn;
861       LogicalPosition newSelectionStart = truncate(selectionStart.line + lineShift, newSelectionStartColumn);
862       LogicalPosition newSelectionEnd = truncate(selectionEnd.line + lineShift, newSelectionEndColumn);
863       newSelectionStartOffset = myEditor.logicalPositionToOffset(newSelectionStart);
864       newSelectionEndOffset = myEditor.logicalPositionToOffset(newSelectionEnd);
865       newSelectionStartPosition = myEditor.logicalToVisualPosition(newSelectionStart);
866       newSelectionEndPosition = myEditor.logicalToVisualPosition(newSelectionEnd);
867       hasNewSelection = !newSelectionStart.equals(newSelectionEnd);
868     }
869     else {
870       newSelectionStartOffset = 0;
871       newSelectionEndOffset = 0;
872       newSelectionStartPosition = null;
873       newSelectionEndPosition = null;
874       hasNewSelection = false;
875       newSelectionStartColumn = -1;
876       newSelectionEndColumn = -1;
877     }
878     LogicalPosition oldPosition = getLogicalPosition();
879     int newLine = oldPosition.line + lineShift;
880     if (newLine < 0 || newLine >= myEditor.getDocument().getLineCount()) {
881       Disposer.dispose(clone);
882       return null;
883     }
884     clone.moveToLogicalPosition(new LogicalPosition(newLine, myLastColumnNumber), false, null, false);
885     clone.myLastColumnNumber = myLastColumnNumber;
886     clone.myDesiredX = myDesiredX >= 0 ? myDesiredX : getCurrentX();
887     clone.myDesiredSelectionStartColumn = newSelectionStartColumn;
888     clone.myDesiredSelectionEndColumn = newSelectionEndColumn;
889
890     if (myEditor.getCaretModel().addCaret(clone, true)) {
891       if (hasNewSelection) {
892         myEditor.getCaretModel().doWithCaretMerging(
893           () -> clone.setSelection(newSelectionStartPosition, newSelectionStartOffset, newSelectionEndPosition, newSelectionEndOffset));
894         if (!clone.isValid()) {
895           return null;
896         }
897       }
898       myEditor.getScrollingModel().scrollTo(clone.getLogicalPosition(), ScrollType.RELATIVE);
899       return clone;
900     }
901     else {
902       Disposer.dispose(clone);
903       return null;
904     }
905   }
906
907   private LogicalPosition truncate(int line, int column) {
908     if (line < 0) {
909       return new LogicalPosition(0, 0);
910     }
911     else if (line >= myEditor.getDocument().getLineCount()) {
912       return myEditor.offsetToLogicalPosition(myEditor.getDocument().getTextLength());
913     }
914     else {
915       return new LogicalPosition(line, column);
916     }
917   }
918
919   /**
920    * @return  information on whether current selection's direction in known
921    * @see #setUnknownDirection(boolean)
922    */
923   boolean isUnknownDirection() {
924     return myUnknownDirection;
925   }
926
927   /**
928    * There is a possible case that we don't know selection's direction. For example, a user might triple-click editor (select the
929    * whole line). We can't say what selection end is a {@link #getLeadSelectionOffset() leading end} then. However, that matters
930    * in a situation when a user clicks before or after that line holding Shift key. It's expected that the selection is expanded
931    * up to that point than.
932    * <p/>
933    * That's why we allow to specify that the direction is unknown and {@link #isUnknownDirection() expose this information}
934    * later.
935    * <p/>
936    * <b>Note:</b> when this method is called with <code>'true'</code>, subsequent calls are guaranteed to return <code>'true'</code>
937    * until selection is changed. 'Unknown direction' flag is automatically reset then.
938    *
939    */
940   void setUnknownDirection(boolean unknownDirection) {
941     myUnknownDirection = unknownDirection;
942   }
943
944   @Override
945   public int getSelectionStart() {
946     validateContext(false);
947     if (hasSelection()) {
948       RangeMarker marker = mySelectionMarker;
949       if (marker != null) {
950         return marker.getStartOffset();
951       }
952     }
953     return getOffset();
954   }
955
956   @NotNull
957   @Override
958   public VisualPosition getSelectionStartPosition() {
959     validateContext(true);
960     VisualPosition position;
961     SelectionMarker marker = mySelectionMarker;
962     if (hasSelection()) {
963       position = getRangeMarkerStartPosition();
964       if (position == null) {
965         VisualPosition startPosition = myEditor.offsetToVisualPosition(marker.getStartOffset(), true, false);
966         VisualPosition endPosition = myEditor.offsetToVisualPosition(marker.getEndOffset(), false, true);
967         position = startPosition.after(endPosition) ? endPosition : startPosition;
968       }
969     }
970     else {
971       position = isVirtualSelectionEnabled() ? getVisualPosition() :
972                  myEditor.offsetToVisualPosition(getOffset(), getLogicalPosition().leansForward, false);
973     }
974     if (hasVirtualSelection()) {
975       position = new VisualPosition(position.line, position.column + marker.startVirtualOffset);
976     }
977     return position;
978   }
979
980   LogicalPosition getSelectionStartLogicalPosition() {
981     validateContext(true);
982     LogicalPosition position;
983     SelectionMarker marker = mySelectionMarker;
984     if (hasSelection()) {
985       VisualPosition visualPosition = getRangeMarkerStartPosition();
986       position = visualPosition == null ? myEditor.offsetToLogicalPosition(marker.getStartOffset()).leanForward(true)
987                                         : myEditor.visualToLogicalPosition(visualPosition);
988     }
989     else {
990       position = getLogicalPosition();
991     }
992     if (hasVirtualSelection()) {
993       position = new LogicalPosition(position.line, position.column + marker.startVirtualOffset);
994     }
995     return position;
996   }
997
998   @Override
999   public int getSelectionEnd() {
1000     validateContext(false);
1001     if (hasSelection()) {
1002       RangeMarker marker = mySelectionMarker;
1003       if (marker != null) {
1004         return marker.getEndOffset();
1005       }
1006     }
1007     return getOffset();
1008   }
1009
1010   @NotNull
1011   @Override
1012   public VisualPosition getSelectionEndPosition() {
1013     validateContext(true);
1014     VisualPosition position;
1015     SelectionMarker marker = mySelectionMarker;
1016     if (hasSelection()) {
1017       position = getRangeMarkerEndPosition();
1018       if (position == null) {
1019         VisualPosition startPosition = myEditor.offsetToVisualPosition(marker.getStartOffset(), true, false);
1020         VisualPosition endPosition = myEditor.offsetToVisualPosition(marker.getEndOffset(), false, true);
1021         position = startPosition.after(endPosition) ? startPosition : endPosition;
1022       }
1023     }
1024     else {
1025       position = isVirtualSelectionEnabled() ? getVisualPosition() :
1026                  myEditor.offsetToVisualPosition(getOffset(), getLogicalPosition().leansForward, false);
1027     }
1028     if (hasVirtualSelection()) {
1029       position = new VisualPosition(position.line, position.column + marker.endVirtualOffset);
1030     }
1031     return position;
1032   }
1033
1034   LogicalPosition getSelectionEndLogicalPosition() {
1035     validateContext(true);
1036     LogicalPosition position;
1037     SelectionMarker marker = mySelectionMarker;
1038     if (hasSelection()) {
1039       VisualPosition visualPosition = getRangeMarkerEndPosition();
1040       position = visualPosition == null ? myEditor.offsetToLogicalPosition(marker.getEndOffset())
1041                                         : myEditor.visualToLogicalPosition(visualPosition);
1042     }
1043     else {
1044       position = getLogicalPosition();
1045     }
1046     if (hasVirtualSelection()) {
1047       position = new LogicalPosition(position.line, position.column + marker.endVirtualOffset);
1048     }
1049     return position;
1050   }
1051
1052   @Override
1053   public boolean hasSelection() {
1054     validateContext(false);
1055     SelectionMarker marker = mySelectionMarker;
1056     return marker != null && marker.isValid() && (marker.getEndOffset() > marker.getStartOffset()
1057                                                   || isVirtualSelectionEnabled() && marker.hasVirtualSelection());
1058   }
1059
1060   @Override
1061   public void setSelection(int startOffset, int endOffset) {
1062     setSelection(startOffset, endOffset, true);
1063   }
1064
1065   @Override
1066   public void setSelection(int startOffset, int endOffset, boolean updateSystemSelection) {
1067     doSetSelection(myEditor.offsetToVisualPosition(startOffset, true, false), startOffset,
1068                    myEditor.offsetToVisualPosition(endOffset, false, true), endOffset,
1069                    false, updateSystemSelection);
1070   }
1071
1072   @Override
1073   public void setSelection(int startOffset, @Nullable VisualPosition endPosition, int endOffset) {
1074     VisualPosition startPosition;
1075     if (hasSelection()) {
1076       startPosition = getLeadSelectionPosition();
1077     }
1078     else {
1079       startPosition = myEditor.offsetToVisualPosition(startOffset, true, false);
1080     }
1081     setSelection(startPosition, startOffset, endPosition, endOffset);
1082   }
1083
1084   @Override
1085   public void setSelection(@Nullable VisualPosition startPosition, int startOffset, @Nullable VisualPosition endPosition, int endOffset) {
1086     setSelection(startPosition, startOffset, endPosition, endOffset, true);
1087   }
1088
1089   @Override
1090   public void setSelection(@Nullable VisualPosition startPosition, int startOffset, @Nullable VisualPosition endPosition, int endOffset,
1091                            boolean updateSystemSelection) {
1092     VisualPosition startPositionToUse = startPosition == null ? myEditor.offsetToVisualPosition(startOffset, true, false) : startPosition;
1093     VisualPosition endPositionToUse = endPosition == null ? myEditor.offsetToVisualPosition(endOffset, false, true) : endPosition;
1094     doSetSelection(startPositionToUse, startOffset, endPositionToUse, endOffset, true, updateSystemSelection);
1095   }
1096
1097   private void doSetSelection(@NotNull final VisualPosition startPosition,
1098                               final int _startOffset,
1099                               @NotNull final VisualPosition endPosition,
1100                               final int _endOffset,
1101                               final boolean visualPositionAware,
1102                               final boolean updateSystemSelection)
1103   {
1104     myEditor.getCaretModel().doWithCaretMerging(() -> {
1105       int startOffset = _startOffset;
1106       int endOffset = _endOffset;
1107       myUnknownDirection = false;
1108       final Document doc = myEditor.getDocument();
1109
1110       validateContext(true);
1111
1112       int textLength = doc.getTextLength();
1113       if (startOffset < 0 || startOffset > textLength) {
1114         LOG.error("Wrong startOffset: " + startOffset + ", textLength=" + textLength);
1115       }
1116       if (endOffset < 0 || endOffset > textLength) {
1117         LOG.error("Wrong endOffset: " + endOffset + ", textLength=" + textLength);
1118       }
1119
1120       if (!visualPositionAware && startOffset == endOffset) {
1121         removeSelection();
1122         return;
1123       }
1124
1125   /* Normalize selection */
1126       boolean switchedOffsets = false;
1127       if (startOffset > endOffset) {
1128         int tmp = startOffset;
1129         startOffset = endOffset;
1130         endOffset = tmp;
1131         switchedOffsets = true;
1132       }
1133
1134       FoldingModelEx foldingModel = myEditor.getFoldingModel();
1135       FoldRegion startFold = foldingModel.getCollapsedRegionAtOffset(startOffset);
1136       if (startFold != null && startFold.getStartOffset() < startOffset) {
1137         startOffset = startFold.getStartOffset();
1138       }
1139
1140       FoldRegion endFold = foldingModel.getCollapsedRegionAtOffset(endOffset);
1141       if (endFold != null && endFold.getStartOffset() < endOffset) {
1142         // All visual positions that lay at collapsed fold region placeholder are mapped to the same offset. Hence, there are
1143         // at least two distinct situations - selection end is located inside collapsed fold region placeholder and just before it.
1144         // We want to expand selection to the fold region end at the former case and keep selection as-is at the latest one.
1145         endOffset = endFold.getEndOffset();
1146       }
1147
1148       int oldSelectionStart;
1149       int oldSelectionEnd;
1150
1151       if (hasSelection()) {
1152         oldSelectionStart = getSelectionStart();
1153         oldSelectionEnd = getSelectionEnd();
1154         if (oldSelectionStart == startOffset && oldSelectionEnd == endOffset && !visualPositionAware) return;
1155       }
1156       else {
1157         oldSelectionStart = oldSelectionEnd = getOffset();
1158       }
1159
1160       SelectionMarker marker = new SelectionMarker(startOffset, endOffset);
1161       if (visualPositionAware) {
1162         if (endPosition.after(startPosition)) {
1163           setRangeMarkerStartPosition(startPosition);
1164           setRangeMarkerEndPosition(endPosition);
1165           setRangeMarkerEndPositionIsLead(false);
1166         }
1167         else {
1168           setRangeMarkerStartPosition(endPosition);
1169           setRangeMarkerEndPosition(startPosition);
1170           setRangeMarkerEndPositionIsLead(true);
1171         }
1172
1173         if (isVirtualSelectionEnabled() &&
1174             myEditor.getDocument().getLineNumber(startOffset) == myEditor.getDocument().getLineNumber(endOffset)) {
1175           int endLineColumn = myEditor.offsetToVisualPosition(endOffset).column;
1176           int startDiff =
1177             EditorUtil.isAtLineEnd(myEditor, switchedOffsets ? endOffset : startOffset) ? startPosition.column - endLineColumn : 0;
1178           int endDiff =
1179             EditorUtil.isAtLineEnd(myEditor, switchedOffsets ? startOffset : endOffset) ? endPosition.column - endLineColumn : 0;
1180           marker.startVirtualOffset = Math.max(0, Math.min(startDiff, endDiff));
1181           marker.endVirtualOffset = Math.max(0, Math.max(startDiff, endDiff));
1182         }
1183       }
1184       mySelectionMarker = marker;
1185
1186       myEditor.getSelectionModel().fireSelectionChanged(oldSelectionStart, oldSelectionEnd, startOffset, endOffset);
1187
1188       if (updateSystemSelection) {
1189         updateSystemSelection();
1190       }
1191     });
1192   }
1193
1194   private void updateSystemSelection() {
1195     if (GraphicsEnvironment.isHeadless()) return;
1196
1197     final Clipboard clip = myEditor.getComponent().getToolkit().getSystemSelection();
1198     if (clip != null) {
1199       clip.setContents(new StringSelection(myEditor.getSelectionModel().getSelectedText(true)), EmptyClipboardOwner.INSTANCE);
1200     }
1201   }
1202
1203   @Override
1204   public void removeSelection() {
1205     if (myEditor.isStickySelection()) {
1206       // Most of our 'change caret position' actions (like move caret to word start/end etc) remove active selection.
1207       // However, we don't want to do that for 'sticky selection'.
1208       return;
1209     }
1210     myEditor.getCaretModel().doWithCaretMerging(() -> {
1211       validateContext(true);
1212       int caretOffset = getOffset();
1213       RangeMarker marker = mySelectionMarker;
1214       if (marker != null && marker.isValid()) {
1215         int startOffset = marker.getStartOffset();
1216         int endOffset = marker.getEndOffset();
1217         mySelectionMarker = null;
1218         myEditor.getSelectionModel().fireSelectionChanged(startOffset, endOffset, caretOffset, caretOffset);
1219       }
1220     });
1221   }
1222
1223   @Override
1224   public int getLeadSelectionOffset() {
1225     validateContext(false);
1226     int caretOffset = getOffset();
1227     if (hasSelection()) {
1228       RangeMarker marker = mySelectionMarker;
1229       if (marker != null && marker.isValid()) {
1230         int startOffset = marker.getStartOffset();
1231         int endOffset = marker.getEndOffset();
1232         if (caretOffset != startOffset && caretOffset != endOffset) {
1233           // Try to check if current selection is tweaked by fold region.
1234           FoldingModelEx foldingModel = myEditor.getFoldingModel();
1235           FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(caretOffset);
1236           if (foldRegion != null) {
1237             if (foldRegion.getStartOffset() == startOffset) {
1238               return endOffset;
1239             }
1240             else if (foldRegion.getEndOffset() == endOffset) {
1241               return startOffset;
1242             }
1243           }
1244         }
1245
1246         if (caretOffset == endOffset) {
1247           return startOffset;
1248         }
1249         else {
1250           return endOffset;
1251         }
1252       }
1253     }
1254     return caretOffset;
1255   }
1256
1257   @NotNull
1258   @Override
1259   public VisualPosition getLeadSelectionPosition() {
1260     SelectionMarker marker = mySelectionMarker;
1261     VisualPosition caretPosition = getVisualPosition();
1262     if (isVirtualSelectionEnabled() && !hasSelection()) {
1263       return caretPosition;
1264     }
1265     if (marker == null || !marker.isValid()) {
1266       return caretPosition;
1267     }
1268
1269     if (isRangeMarkerEndPositionIsLead()) {
1270       VisualPosition result = getRangeMarkerEndPosition();
1271       if (result == null) {
1272         return getSelectionEndPosition();
1273       }
1274       else {
1275         if (hasVirtualSelection()) {
1276           result = new VisualPosition(result.line, result.column + marker.endVirtualOffset);
1277         }
1278         return result;
1279       }
1280     }
1281     else {
1282       VisualPosition result = getRangeMarkerStartPosition();
1283       if (result == null) {
1284         return getSelectionStartPosition();
1285       }
1286       else {
1287         if (hasVirtualSelection()) {
1288           result = new VisualPosition(result.line, result.column + marker.startVirtualOffset);
1289         }
1290         return result;
1291       }
1292     }
1293   }
1294
1295   @Override
1296   public void selectLineAtCaret() {
1297     validateContext(true);
1298     myEditor.getCaretModel().doWithCaretMerging(() -> SelectionModelImpl.doSelectLineAtCaret(this));
1299   }
1300
1301   @Override
1302   public void selectWordAtCaret(final boolean honorCamelWordsSettings) {
1303     validateContext(true);
1304     myEditor.getCaretModel().doWithCaretMerging(() -> {
1305       removeSelection();
1306       final EditorSettings settings = myEditor.getSettings();
1307       boolean camelTemp = settings.isCamelWords();
1308
1309       final boolean needOverrideSetting = camelTemp && !honorCamelWordsSettings;
1310       if (needOverrideSetting) {
1311         settings.setCamelWords(false);
1312       }
1313
1314       try {
1315         EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET);
1316         handler.execute(myEditor, this, myEditor.getDataContext());
1317       }
1318       finally {
1319         if (needOverrideSetting) {
1320           settings.resetCamelWords();
1321         }
1322       }
1323     });
1324   }
1325
1326   @Nullable
1327   @Override
1328   public String getSelectedText() {
1329     if (!hasSelection()) {
1330       return null;
1331     }
1332     SelectionMarker selectionMarker = mySelectionMarker;
1333     CharSequence text = myEditor.getDocument().getCharsSequence();
1334     int selectionStart = getSelectionStart();
1335     int selectionEnd = getSelectionEnd();
1336     String selectedText = text.subSequence(selectionStart, selectionEnd).toString();
1337     if (isVirtualSelectionEnabled() && selectionMarker.hasVirtualSelection()) {
1338       int padding = selectionMarker.endVirtualOffset - selectionMarker.startVirtualOffset;
1339       StringBuilder builder = new StringBuilder(selectedText.length() + padding);
1340       builder.append(selectedText);
1341       for (int i = 0; i < padding; i++) {
1342         builder.append(' ');
1343       }
1344       return builder.toString();
1345     }
1346     else {
1347       return selectedText;
1348     }
1349   }
1350
1351   private static void validateContext(boolean requireEdt) {
1352     if (requireEdt) {
1353       ApplicationManager.getApplication().assertIsDispatchThread();
1354     }
1355     else {
1356       ApplicationManager.getApplication().assertReadAccessAllowed();
1357     }
1358   }
1359
1360   private boolean isVirtualSelectionEnabled() {
1361     return myEditor.isColumnMode();
1362   }
1363
1364   boolean hasVirtualSelection() {
1365     validateContext(false);
1366     SelectionMarker marker = mySelectionMarker;
1367     return marker != null && marker.isValid() && isVirtualSelectionEnabled() && marker.hasVirtualSelection();
1368   }
1369
1370   void resetVirtualSelection() {
1371     SelectionMarker marker = mySelectionMarker;
1372     if (marker != null) marker.resetVirtualSelection();
1373   }
1374
1375   private int getCurrentX() {
1376     return myEditor.visualPositionToXY(myVisibleCaret).x;
1377   }
1378
1379   @Override
1380   @NotNull
1381   public EditorImpl getEditor() {
1382     return myEditor;
1383   }
1384
1385   @Override
1386   public String toString() {
1387     return "Caret at " + (myDocumentUpdateCounter == myEditor.getCaretModel().myDocumentUpdateCounter ? myVisibleCaret : getOffset()) +
1388            (mySelectionMarker == null ? "" : (", selection marker: " + mySelectionMarker.toString()));
1389   }
1390
1391   @Override
1392   public boolean isAtRtlLocation() {
1393     return myEditor.myUseNewRendering && myEditor.myView.isRtlLocation(getVisualPosition());
1394   }
1395
1396   @Override
1397   public boolean isAtBidiRunBoundary() {
1398     return myEditor.myUseNewRendering && myEditor.myView.isAtBidiRunBoundary(getVisualPosition());
1399   }
1400
1401   @NotNull
1402   @Override
1403   public String dumpState() {
1404     return "{valid: " + isValid +
1405            ", update counter: " + myDocumentUpdateCounter +
1406            ", position: " + myPositionMarker +
1407            ", logical pos: " + myLogicalCaret +
1408            ", visual pos: " + myVisibleCaret +
1409            ", visual line start: " + myVisualLineStart +
1410            ", visual line end: " + myVisualLineEnd +
1411            ", skip change requests: " + mySkipChangeRequests +
1412            ", desired selection start column: " + myDesiredSelectionStartColumn +
1413            ", desired selection end column: " + myDesiredSelectionEndColumn +
1414            ", report caret moves: " + myReportCaretMoves +
1415            ", desired x: " + myDesiredX +
1416            ", selection marker: " + mySelectionMarker +
1417            ", rangeMarker start position: " + myRangeMarkerStartPosition +
1418            ", rangeMarker end position: " + myRangeMarkerEndPosition +
1419            ", rangeMarker end position is lead: " + myRangeMarkerEndPositionIsLead +
1420            ", unknown direction: " + myUnknownDirection +
1421            ", virtual space offset: " + myVirtualSpaceOffset + '}';
1422   }
1423
1424   /**
1425    * Encapsulates information about target vertical range info - its <code>'y'</code> coordinate and height in pixels.
1426    */
1427   private static class VerticalInfo {
1428     public final int y;
1429     public final int height;
1430
1431     private VerticalInfo(int y, int height) {
1432       this.y = y;
1433       this.height = height;
1434     }
1435   }
1436
1437   @Nullable
1438   private VisualPosition getRangeMarkerStartPosition() {
1439     invalidateRangeMarkerVisualPositions(mySelectionMarker);
1440     return myRangeMarkerStartPosition;
1441   }
1442
1443   private void setRangeMarkerStartPosition(@NotNull VisualPosition startPosition) {
1444     myRangeMarkerStartPosition = startPosition;
1445   }
1446
1447   @Nullable
1448   private VisualPosition getRangeMarkerEndPosition() {
1449     invalidateRangeMarkerVisualPositions(mySelectionMarker);
1450     return myRangeMarkerEndPosition;
1451   }
1452
1453   void setRangeMarkerEndPosition(@NotNull VisualPosition endPosition) {
1454     myRangeMarkerEndPosition = endPosition;
1455   }
1456
1457   private boolean isRangeMarkerEndPositionIsLead() {
1458     return myRangeMarkerEndPositionIsLead;
1459   }
1460
1461   void setRangeMarkerEndPositionIsLead(boolean endPositionIsLead) {
1462     myRangeMarkerEndPositionIsLead = endPositionIsLead;
1463   }
1464
1465   private void invalidateRangeMarkerVisualPositions(RangeMarker marker) {
1466     SoftWrapModelImpl model = myEditor.getSoftWrapModel();
1467     InlayModelImpl inlayModel = myEditor.getInlayModel();
1468     int startOffset = marker.getStartOffset();
1469     int endOffset = marker.getEndOffset();
1470     if (!myEditor.offsetToVisualPosition(startOffset, true, false).equals(myRangeMarkerStartPosition) &&
1471         model.getSoftWrap(startOffset) == null && !inlayModel.hasInlineElementAt(startOffset) ||
1472         !myEditor.offsetToVisualPosition(endOffset, false, true).equals(myRangeMarkerEndPosition)
1473         && model.getSoftWrap(endOffset) == null && !inlayModel.hasInlineElementAt(endOffset)) {
1474       myRangeMarkerStartPosition = null;
1475       myRangeMarkerEndPosition = null;
1476     }
1477   }
1478
1479   void updateCachedStateIfNeeded() {
1480     if (!ApplicationManager.getApplication().isDispatchThread()) return;
1481     int modelCounter = myEditor.getCaretModel().myDocumentUpdateCounter;
1482     if (myDocumentUpdateCounter != modelCounter) {
1483       LogicalPosition lp = myEditor.offsetToLogicalPosition(getOffset());
1484       setCurrentLogicalCaret(new LogicalPosition(lp.line, lp.column + myVirtualSpaceOffset, myLeansTowardsLargerOffsets));
1485       myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
1486       updateVisualLineInfo();
1487       setLastColumnNumber(myLogicalCaret.column);
1488       myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
1489       myDesiredX = -1;
1490       myDocumentUpdateCounter = modelCounter;
1491     }
1492   }
1493
1494   class PositionMarker extends RangeMarkerImpl {
1495     private PositionMarker(int offset) {
1496       super(myEditor.getDocument(), offset, offset, false);
1497       myEditor.getCaretModel().myPositionMarkerTree.addInterval(this, offset, offset, false, false, 0);
1498     }
1499
1500     @Override
1501     public void dispose() {
1502       if (isValid()) {
1503         myEditor.getCaretModel().myPositionMarkerTree.removeInterval(this);
1504       }
1505     }
1506
1507     @Override
1508     protected void changedUpdateImpl(@NotNull DocumentEvent e) {
1509       int oldOffset = intervalStart();
1510       super.changedUpdateImpl(e);
1511       if (isValid()) {
1512         // Under certain conditions, when text is inserted at caret position, we position caret at the end of inserted text.
1513         // Ideally, client code should be responsible for positioning caret after document modification, but in case of
1514         // postponed formatting (after PSI modifications), this is hard to implement, so a heuristic below is used.
1515         if (e.getOldLength() == 0 && oldOffset == e.getOffset() && needToShiftWhiteSpaces(e)) {
1516           int afterInserted = e.getOffset() + e.getNewLength();
1517           setIntervalStart(afterInserted);
1518           setIntervalEnd(afterInserted);
1519         }
1520       }
1521       else {
1522         setValid(true);
1523         int newOffset = Math.min(intervalStart(), e.getOffset() + e.getNewLength());
1524         if (!((DocumentEx)e.getDocument()).isInBulkUpdate() && e.isWholeTextReplaced()) {
1525           try {
1526             final int line = ((DocumentEventImpl)e).translateLineViaDiff(myLogicalCaret.line);
1527             newOffset = myEditor.logicalPositionToOffset(new LogicalPosition(line, myLogicalCaret.column));
1528           }
1529           catch (FilesTooBigForDiffException ex) {
1530             LOG.info(ex);
1531           }
1532         }
1533         setIntervalStart(newOffset);
1534         setIntervalEnd(newOffset);
1535       }
1536       if (oldOffset >= e.getOffset() && oldOffset <= (e.getOffset() + e.getOldLength()) && e.getNewLength() == 0 &&
1537           myEditor.getInlayModel().hasInlineElementAt(e.getOffset())) {
1538         myLeansTowardsLargerOffsets = true;
1539       }
1540       else if (oldOffset == e.getOffset()) {
1541         myLeansTowardsLargerOffsets = false;
1542       }
1543     }
1544
1545     private boolean needToShiftWhiteSpaces(final DocumentEvent e) {
1546       return e.getOffset() > 0 && Character.isWhitespace(e.getDocument().getImmutableCharSequence().charAt(e.getOffset() - 1)) &&
1547              CharArrayUtil.containsOnlyWhiteSpaces(e.getNewFragment()) && !CharArrayUtil.containLineBreaks(e.getNewFragment());
1548     }
1549   }
1550
1551   class SelectionMarker extends RangeMarkerImpl {
1552     // offsets of selection start/end position relative to end of line - can be non-zero in column selection mode
1553     // these are non-negative values, myStartVirtualOffset is always less or equal to myEndVirtualOffset
1554     private int startVirtualOffset;
1555     private int endVirtualOffset;
1556
1557     private SelectionMarker(int start, int end) {
1558       super(myEditor.getDocument(), start, end, false);
1559       myEditor.getCaretModel().mySelectionMarkerTree.addInterval(this, start, end, false, false, 0);
1560     }
1561
1562     private void resetVirtualSelection() {
1563       startVirtualOffset = 0;
1564       endVirtualOffset = 0;
1565     }
1566
1567     private boolean hasVirtualSelection() {
1568       return endVirtualOffset > startVirtualOffset;
1569     }
1570
1571     @Override
1572     public void dispose() {
1573       if (isValid()) {
1574         myEditor.getCaretModel().mySelectionMarkerTree.removeInterval(this);
1575       }
1576     }
1577
1578     @Override
1579     protected void changedUpdateImpl(@NotNull DocumentEvent e) {
1580       super.changedUpdateImpl(e);
1581       if (endVirtualOffset > 0 && isValid()) {
1582         Document document = e.getDocument();
1583         int startAfter = intervalStart();
1584         int endAfter = intervalEnd();
1585         if (!DocumentUtil.isAtLineEnd(endAfter, document) || document.getLineNumber(startAfter) != document.getLineNumber(endAfter)) {
1586           resetVirtualSelection();
1587         }
1588       }
1589     }
1590
1591     @Override
1592     public String toString() {
1593       return super.toString() + (hasVirtualSelection() ? (" virtual selection: " + startVirtualOffset + "-" + endVirtualOffset) : "");
1594     }
1595   }
1596 }