/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor.impl;
import com.intellij.diagnostic.Dumpable;
import com.intellij.diagnostic.LogMessageEx;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actions.EditorActionUtil;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
import com.intellij.openapi.editor.ex.FoldingModelEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapHelper;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.DocumentUtil;
import com.intellij.util.diff.FilesTooBigForDiffException;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.EmptyClipboardOwner;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
public class CaretImpl extends UserDataHolderBase implements Caret, Dumpable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.CaretImpl");
private static final Key<CaretVisualAttributes> VISUAL_ATTRIBUTES_KEY = new Key<>("CaretAttributes");
private final EditorImpl myEditor;
private boolean isValid = true;
private LogicalPosition myLogicalCaret;
private VerticalInfo myCaretInfo;
private VisualPosition myVisibleCaret;
private volatile PositionMarker myPositionMarker;
private boolean myLeansTowardsLargerOffsets;
private int myVirtualSpaceOffset;
private int myVisualLineStart;
private int myVisualLineEnd;
private boolean mySkipChangeRequests;
/**
* Initial horizontal caret position during vertical navigation.
* Similar to {@link #myDesiredX}, but represents logical caret position ({@code getLogicalPosition().column}) rather than visual.
*/
private int myLastColumnNumber;
private int myDesiredSelectionStartColumn = -1;
private int myDesiredSelectionEndColumn = -1;
/**
* We check that caret is located at the target offset at the end of {@link #moveToOffset(int, boolean)} method. However,
* it's possible that the following situation occurs:
* <p/>
* <pre>
* <ol>
* <li>Some client subscribes to caret change events;</li>
* <li>{@link #moveToLogicalPosition(LogicalPosition)} is called;</li>
* <li>Caret position is changed during {@link #moveToLogicalPosition(LogicalPosition)} processing;</li>
* <li>The client receives caret position change event and adjusts the position;</li>
* <li>{@link #moveToLogicalPosition(LogicalPosition)} processing is finished;</li>
* <li>{@link #moveToLogicalPosition(LogicalPosition)} reports an error because the caret is not located at the target offset;</li>
* </ol>
* </pre>
* <p/>
* This field serves as a flag that reports unexpected caret position change requests nested from {@link #moveToOffset(int, boolean)}.
*/
private boolean myReportCaretMoves;
/**
* This field holds initial horizontal caret position during vertical navigation. It's used to determine target position when
* moving to the new line. It is stored in pixels, not in columns, to account for non-monospaced fonts as well.
* <p/>
* Negative value means no coordinate should be preserved.
*/
private int myDesiredX = -1;
private volatile SelectionMarker mySelectionMarker;
private volatile VisualPosition myRangeMarkerStartPosition;
private volatile VisualPosition myRangeMarkerEndPosition;
private volatile boolean myRangeMarkerEndPositionIsLead;
private boolean myUnknownDirection;
private int myDocumentUpdateCounter;
CaretImpl(EditorImpl editor) {
myEditor = editor;
myLogicalCaret = new LogicalPosition(0, 0);
myVisibleCaret = new VisualPosition(0, 0);
myCaretInfo = new VerticalInfo(0, 0);
myPositionMarker = new PositionMarker(0);
myVisualLineStart = 0;
Document doc = myEditor.getDocument();
myVisualLineEnd = doc.getLineCount() > 1 ? doc.getLineStartOffset(1) : doc.getLineCount() == 0 ? 0 : doc.getLineEndOffset(0);
myDocumentUpdateCounter = editor.getCaretModel().myDocumentUpdateCounter;
}
@Override
public void moveToOffset(int offset) {
moveToOffset(offset, false);
}
@Override
public void moveToOffset(final int offset, final boolean locateBeforeSoftWrap) {
assertIsDispatchThread();
validateCallContext();
if (mySkipChangeRequests) {
return;
}
myEditor.getCaretModel().doWithCaretMerging(() -> {
final LogicalPosition logicalPosition = myEditor.offsetToLogicalPosition(offset);
CaretEvent event = moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, null, false);
final LogicalPosition positionByOffsetAfterMove = myEditor.offsetToLogicalPosition(getOffset());
if (!positionByOffsetAfterMove.equals(logicalPosition)) {
StringBuilder debugBuffer = new StringBuilder();
moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, debugBuffer, true);
int actualOffset = getOffset();
int textStart = Math.max(0, Math.min(offset, actualOffset) - 1);
final DocumentEx document = myEditor.getDocument();
int textEnd = Math.min(document.getTextLength() - 1, Math.max(offset, actualOffset) + 1);
CharSequence text = document.getCharsSequence().subSequence(textStart, textEnd);
int inverseOffset = myEditor.logicalPositionToOffset(logicalPosition);
LogMessageEx.error(
LOG, "caret moved to wrong offset. Please submit a dedicated ticket and attach current editor's text to it.",
"Requested: offset=" + offset + ", logical position='" + logicalPosition + "' but actual: offset=" +
actualOffset + ", logical position='" + myLogicalCaret + "' (" + positionByOffsetAfterMove + "). " + myEditor.dumpState() +
"\ninterested text [" + textStart + ";" + textEnd + "): '" + text + "'\n debug trace: " + debugBuffer +
"\nLogical position -> offset ('" + logicalPosition + "'->'" + inverseOffset + "')"
);
}
if (event != null) {
myEditor.getCaretModel().fireCaretPositionChanged(event);
EditorActionUtil.selectNonexpandableFold(myEditor);
}
});
}
@NotNull
@Override
public CaretModel getCaretModel() {
return myEditor.getCaretModel();
}
@Override
public boolean isValid() {
return isValid;
}
@Override
public void moveCaretRelatively(final int _columnShift, final int lineShift, final boolean withSelection, final boolean scrollToCaret) {
assertIsDispatchThread();
if (mySkipChangeRequests) {
return;
}
if (myReportCaretMoves) {
LogMessageEx.error(LOG, "Unexpected caret move request");
}
if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling()) {
CopyPasteManager.getInstance().stopKillRings();
}
myEditor.getCaretModel().doWithCaretMerging(() -> {
updateCachedStateIfNeeded();
int oldOffset = getOffset();
int columnShift = _columnShift;
if (withSelection && lineShift == 0) {
if (columnShift == -1) {
int column = myVisibleCaret.column - (hasSelection() && oldOffset == getSelectionEnd() ? 2 : 1);
if (column >= 0 && myEditor.getInlayModel().hasInlineElementAt(new VisualPosition(myVisibleCaret.line, column))) {
columnShift = -2;
}
}
else if (columnShift == 1) {
if (myEditor.getInlayModel().hasInlineElementAt(
new VisualPosition(myVisibleCaret.line, myVisibleCaret.column + (hasSelection() && oldOffset == getSelectionStart() ? 1 : 0)))) {
columnShift = 2;
}
}
}
final int leadSelectionOffset = getLeadSelectionOffset();
final VisualPosition leadSelectionPosition = getLeadSelectionPosition();
EditorSettings editorSettings = myEditor.getSettings();
VisualPosition visualCaret = getVisualPosition();
int lastColumnNumber = myLastColumnNumber;
int desiredX = myDesiredX;
if (columnShift == 0) {
if (myDesiredX < 0) {
desiredX = getCurrentX();
}
}
else {
myDesiredX = desiredX = -1;
}
int newLineNumber = visualCaret.line + lineShift;
int newColumnNumber = visualCaret.column + columnShift;
boolean newLeansRight = lineShift == 0 && columnShift != 0 ? columnShift < 0 : visualCaret.leansRight;
if (desiredX >= 0) {
newColumnNumber = myEditor.xyToVisualPosition(new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight())).column;
}
Document document = myEditor.getDocument();
if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == 1) {
int lastLine = document.getLineCount() - 1;
if (lastLine < 0) lastLine = 0;
if (newColumnNumber > EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber) &&
newLineNumber < myEditor.logicalToVisualPosition(new LogicalPosition(lastLine, 0)).line) {
newColumnNumber = 0;
newLineNumber++;
}
}
else if (!editorSettings.isVirtualSpace() && lineShift == 0 && columnShift == -1) {
if (newColumnNumber < 0 && newLineNumber > 0) {
newLineNumber--;
newColumnNumber = EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber);
}
}
if (newColumnNumber < 0) newColumnNumber = 0;
// There is a possible case that caret is located at the first line and user presses 'Shift+Up'. We want to select all text
// from the document start to the current caret position then. So, we have a dedicated flag for tracking that.
boolean selectToDocumentStart = false;
if (newLineNumber < 0) {
selectToDocumentStart = true;
newLineNumber = 0;
// We want to move caret to the first column if it's already located at the first line and 'Up' is pressed.
newColumnNumber = 0;
}
VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber);
if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber, newLeansRight));
int offset = myEditor.logicalPositionToOffset(log);
if (offset >= document.getTextLength() && columnShift == 0) {
int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength(), true, false).column;
// We want to move caret to the last column if if it's located at the last line and 'Down' is pressed.
if (lastOffsetColumn > newColumnNumber) {
newColumnNumber = lastOffsetColumn;
newLeansRight = true;
}
}
if (!editorSettings.isCaretInsideTabs()) {
CharSequence text = document.getCharsSequence();
if (offset >= 0 && offset < document.getTextLength()) {
if (text.charAt(offset) == '\t' && (columnShift <= 0 || offset == oldOffset)) {
if (columnShift <= 0) {
newColumnNumber = myEditor.offsetToVisualPosition(offset, true, false).column;
}
else {
SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset + 1);
// There is a possible case that tabulation symbol is the last document symbol represented on a visual line before
// soft wrap. We can't just use column from 'offset + 1' because it would point on a next visual line.
if (softWrap == null) {
newColumnNumber = myEditor.offsetToVisualPosition(offset + 1).column;
}
else {
newColumnNumber = EditorUtil.getLastVisualLineColumnNumber(myEditor, newLineNumber);
}
}
}
}
}
}
pos = new VisualPosition(newLineNumber, newColumnNumber, newLeansRight);
if (columnShift != 0 && lineShift == 0 && myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
LogicalPosition logical = myEditor.visualToLogicalPosition(pos);
int softWrapOffset = myEditor.logicalPositionToOffset(logical);
if (columnShift >= 0) {
moveToOffset(softWrapOffset);
}
else {
int line = myEditor.offsetToVisualLine(softWrapOffset - 1);
moveToVisualPosition(new VisualPosition(line, EditorUtil.getLastVisualLineColumnNumber(myEditor, line)));
}
}
else {
moveToVisualPosition(pos);
if (!editorSettings.isVirtualSpace() && columnShift == 0 && lastColumnNumber >=0) {
setLastColumnNumber(lastColumnNumber);
}
}
if (withSelection) {
if (selectToDocumentStart) {
setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(0), 0);
}
else if (pos.line >= myEditor.getVisibleLineCount()) {
int endOffset = document.getTextLength();
if (leadSelectionOffset < endOffset) {
setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(endOffset), endOffset);
}
}
else {
int selectionStartToUse = leadSelectionOffset;
VisualPosition selectionStartPositionToUse = leadSelectionPosition;
if (isUnknownDirection() || oldOffset > getSelectionStart() && oldOffset < getSelectionEnd()) {
if (getOffset() > leadSelectionOffset ^ getSelectionStart() < getSelectionEnd()) {
selectionStartToUse = getSelectionEnd();
selectionStartPositionToUse = getSelectionEndPosition();
}
else {
selectionStartToUse = getSelectionStart();
selectionStartPositionToUse = getSelectionStartPosition();
}
}
setSelection(selectionStartPositionToUse, selectionStartToUse, getVisualPosition(), getOffset());
}
}
else {
removeSelection();
}
if (scrollToCaret) {
myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
if (desiredX >= 0) {
myDesiredX = desiredX;
}
EditorActionUtil.selectNonexpandableFold(myEditor);
});
}
@Override
public void moveToLogicalPosition(@NotNull final LogicalPosition pos) {
myEditor.getCaretModel().doWithCaretMerging(() -> moveToLogicalPosition(pos, false, null, true));
}
private CaretEvent doMoveToLogicalPosition(@NotNull LogicalPosition pos,
boolean locateBeforeSoftWrap,
@NonNls @Nullable StringBuilder debugBuffer,
boolean fireListeners) {
assertIsDispatchThread();
updateCachedStateIfNeeded();
if (debugBuffer != null) {
debugBuffer.append("Start moveToLogicalPosition(). Locate before soft wrap: ").append(locateBeforeSoftWrap).append(", position: ")
.append(pos).append("\n");
}
myDesiredX = -1;
validateCallContext();
int column = pos.column;
int line = pos.line;
int softWrapLinesBefore = pos.softWrapLinesBeforeCurrentLogicalLine;
int softWrapLinesCurrent = pos.softWrapLinesOnCurrentLogicalLine;
int softWrapColumns = pos.softWrapColumnDiff;
boolean leansForward = pos.leansForward;
boolean leansRight = pos.visualPositionLeansRight;
Document doc = myEditor.getDocument();
int lineCount = doc.getLineCount();
if (lineCount == 0) {
if (debugBuffer != null) {
debugBuffer.append("Resetting target logical line to zero as the document is empty\n");
}
line = 0;
}
else if (line > lineCount - 1) {
if (debugBuffer != null) {
debugBuffer.append("Resetting target logical line (").append(line).append(") to ").append(lineCount - 1)
.append(" as it is greater than total document lines number\n");
}
line = lineCount - 1;
softWrapLinesBefore = 0;
softWrapLinesCurrent = 0;
}
EditorSettings editorSettings = myEditor.getSettings();
if (!editorSettings.isVirtualSpace() && line < lineCount) {
int lineEndOffset = doc.getLineEndOffset(line);
final LogicalPosition endLinePosition = myEditor.offsetToLogicalPosition(lineEndOffset);
int lineEndColumnNumber = endLinePosition.column;
if (column > lineEndColumnNumber) {
int oldColumn = column;
column = lineEndColumnNumber;
leansForward = true;
leansRight = true;
if (softWrapColumns != 0) {
softWrapColumns -= column - lineEndColumnNumber;
}
if (debugBuffer != null) {
debugBuffer.append("Resetting target logical column (").append(oldColumn).append(") to ").append(lineEndColumnNumber)
.append(" because caret is not allowed to be located after line end (offset: ").append(lineEndOffset).append(", ")
.append("logical position: ").append(endLinePosition).append("). Current soft wrap columns value: ").append(softWrapColumns)
.append("\n");
}
}
}
myEditor.getFoldingModel().flushCaretPosition(this);
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition oldCaretPosition = myLogicalCaret;
VisualPosition oldVisualPosition = myVisibleCaret;
LogicalPosition logicalPositionToUse;
if (pos.visualPositionAware) {
logicalPositionToUse = new LogicalPosition(
line, column, softWrapLinesBefore, softWrapLinesCurrent, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff,
leansForward, leansRight
);
}
else {
logicalPositionToUse = new LogicalPosition(line, column, leansForward);
}
final int offset = myEditor.logicalPositionToOffset(logicalPositionToUse);
if (debugBuffer != null) {
debugBuffer.append("Resulting logical position to use: ").append(logicalPositionToUse).append(". It's mapped to offset ").append(offset).append("\n");
}
FoldRegion collapsedAt = myEditor.getFoldingModel().getCollapsedRegionAtOffset(offset);
if (collapsedAt != null && offset > collapsedAt.getStartOffset()) {
if (debugBuffer != null) {
debugBuffer.append("Scheduling expansion of fold region ").append(collapsedAt).append("\n");
}
Runnable runnable = () -> {
FoldRegion[] allCollapsedAt = myEditor.getFoldingModel().fetchCollapsedAt(offset);
for (FoldRegion foldRange : allCollapsedAt) {
foldRange.setExpanded(true);
}
};
mySkipChangeRequests = true;
try {
myEditor.getFoldingModel().runBatchFoldingOperation(runnable, false);
}
finally {
mySkipChangeRequests = false;
}
logicalPositionToUse = logicalPositionToUse.visualPositionAware ? logicalPositionToUse.withoutVisualPositionInfo() : logicalPositionToUse;
}
setCurrentLogicalCaret(logicalPositionToUse);
setLastColumnNumber(myLogicalCaret.column);
myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
updateOffsetsFromLogicalPosition();
int newOffset = getOffset();
if (debugBuffer != null) {
debugBuffer.append("Storing offset ").append(newOffset).append(" (mapped from logical position ").append(myLogicalCaret).append(")\n");
}
updateVisualLineInfo();
myEditor.updateCaretCursor();
requestRepaint(oldInfo);
if (locateBeforeSoftWrap && SoftWrapHelper.isCaretAfterSoftWrap(this)) {
int lineToUse = myVisibleCaret.line - 1;
if (lineToUse >= 0) {
final VisualPosition visualPosition = new VisualPosition(lineToUse, EditorUtil.getLastVisualLineColumnNumber(myEditor, lineToUse));
if (debugBuffer != null) {
debugBuffer.append("Adjusting caret position by moving it before soft wrap. Moving to visual position ").append(visualPosition).append("\n");
}
final LogicalPosition logicalPosition = myEditor.visualToLogicalPosition(visualPosition);
final int tmpOffset = myEditor.logicalPositionToOffset(logicalPosition);
if (tmpOffset == newOffset) {
boolean restore = myReportCaretMoves;
myReportCaretMoves = false;
try {
moveToVisualPosition(visualPosition);
return null;
}
finally {
myReportCaretMoves = restore;
}
}
else {
LogMessageEx.error(LOG, "Invalid editor dimension mapping", "Expected to map visual position '" +
visualPosition + "' to offset " + newOffset + " but got the following: -> logical position '" +
logicalPosition + "'; -> offset " + tmpOffset + ". State: " + myEditor.dumpState());
}
}
}
if (!oldVisualPosition.equals(myVisibleCaret)) {
CaretEvent event = new CaretEvent(myEditor, this, oldCaretPosition, myLogicalCaret);
if (fireListeners) {
myEditor.getCaretModel().fireCaretPositionChanged(event);
}
else {
return event;
}
}
return null;
}
private void updateOffsetsFromLogicalPosition() {
int offset = myEditor.logicalPositionToOffset(myLogicalCaret);
myPositionMarker = new PositionMarker(offset);
myLeansTowardsLargerOffsets = myLogicalCaret.leansForward;
myVirtualSpaceOffset = myLogicalCaret.column - myEditor.offsetToLogicalPosition(offset).column;
}
private void setLastColumnNumber(int lastColumnNumber) {
myLastColumnNumber = lastColumnNumber;
}
private void requestRepaint(VerticalInfo oldCaretInfo) {
int lineHeight = myEditor.getLineHeight();
Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
final EditorGutterComponentEx gutter = myEditor.getGutterComponentEx();
final EditorComponentImpl content = myEditor.getContentComponent();
int updateWidth = myEditor.getScrollPane().getHorizontalScrollBar().getValue() + visibleArea.width;
if (Math.abs(myCaretInfo.y - oldCaretInfo.y) <= 2 * lineHeight) {
int minY = Math.min(oldCaretInfo.y, myCaretInfo.y);
int maxY = Math.max(oldCaretInfo.y + oldCaretInfo.height, myCaretInfo.y + myCaretInfo.height);
content.repaintEditorComponent(0, minY, updateWidth, maxY - minY);
gutter.repaint(0, minY, gutter.getWidth(), maxY - minY);
}
else {
content.repaintEditorComponent(0, oldCaretInfo.y, updateWidth, oldCaretInfo.height + lineHeight);
gutter.repaint(0, oldCaretInfo.y, updateWidth, oldCaretInfo.height + lineHeight);
content.repaintEditorComponent(0, myCaretInfo.y, updateWidth, myCaretInfo.height + lineHeight);
gutter.repaint(0, myCaretInfo.y, updateWidth, myCaretInfo.height + lineHeight);
}
}
@Override
public void moveToVisualPosition(@NotNull final VisualPosition pos) {
myEditor.getCaretModel().doWithCaretMerging(() -> moveToVisualPosition(pos, true));
}
void moveToVisualPosition(@NotNull VisualPosition pos, boolean fireListeners) {
assertIsDispatchThread();
validateCallContext();
if (mySkipChangeRequests) {
return;
}
if (myReportCaretMoves) {
LogMessageEx.error(LOG, "Unexpected caret move request");
}
if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling() && !pos.equals(myVisibleCaret)) {
CopyPasteManager.getInstance().stopKillRings();
}
updateCachedStateIfNeeded();
myDesiredX = -1;
int column = pos.column;
int line = pos.line;
boolean leanRight = pos.leansRight;
int lastLine = myEditor.getVisibleLineCount() - 1;
if (lastLine <= 0) {
lastLine = 0;
}
if (line > lastLine) {
line = lastLine;
}
EditorSettings editorSettings = myEditor.getSettings();
if (!editorSettings.isVirtualSpace()) {
int lineEndColumn = EditorUtil.getLastVisualLineColumnNumber(myEditor, line);
if (column > lineEndColumn && !myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
column = lineEndColumn;
leanRight = true;
}
}
myVisibleCaret = new VisualPosition(line, column, leanRight);
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition oldPosition = myLogicalCaret;
setCurrentLogicalCaret(myEditor.visualToLogicalPosition(myVisibleCaret));
updateOffsetsFromLogicalPosition();
updateVisualLineInfo();
myEditor.getFoldingModel().flushCaretPosition(this);
setLastColumnNumber(myLogicalCaret.column);
myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
myEditor.updateCaretCursor();
requestRepaint(oldInfo);
if (fireListeners && !oldPosition.equals(myLogicalCaret)) {
CaretEvent event = new CaretEvent(myEditor, this, oldPosition, myLogicalCaret);
myEditor.getCaretModel().fireCaretPositionChanged(event);
}
}
@Nullable
CaretEvent moveToLogicalPosition(@NotNull LogicalPosition pos,
boolean locateBeforeSoftWrap,
@Nullable StringBuilder debugBuffer,
boolean fireListeners) {
if (mySkipChangeRequests) {
return null;
}
if (myReportCaretMoves) {
LogMessageEx.error(LOG, "Unexpected caret move request");
}
if (!myEditor.isStickySelection() && !myEditor.getDocument().isInEventsHandling() && !pos.equals(myLogicalCaret)) {
CopyPasteManager.getInstance().stopKillRings();
}
myReportCaretMoves = true;
try {
return doMoveToLogicalPosition(pos, locateBeforeSoftWrap, debugBuffer, fireListeners);
}
finally {
myReportCaretMoves = false;
}
}
private static void assertIsDispatchThread() {
EditorImpl.assertIsDispatchThread();
}
private void validateCallContext() {
LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread() || !myEditor.getCaretModel().myIsInUpdate,
"Caret model is in its update process. All requests are illegal at this point.");
}
@Override
public void dispose() {
if (myPositionMarker != null) {
myPositionMarker = null;
}
if (mySelectionMarker != null) {
mySelectionMarker = null;
}
isValid = false;
}
@Override
public boolean isUpToDate() {
return !myEditor.getCaretModel().myIsInUpdate && !myReportCaretMoves;
}
@NotNull
@Override
public LogicalPosition getLogicalPosition() {
validateCallContext();
updateCachedStateIfNeeded();
return myLogicalCaret;
}
@NotNull
@Override
public VisualPosition getVisualPosition() {
validateCallContext();
updateCachedStateIfNeeded();
return myVisibleCaret;
}
@Override
public int getOffset() {
validateCallContext();
validateContext(false);
PositionMarker marker = myPositionMarker;
if (marker == null) return 0; // caret was disposed
assert marker.isValid();
return marker.getStartOffset();
}
@Override
public int getVisualLineStart() {
updateCachedStateIfNeeded();
return myVisualLineStart;
}
@Override
public int getVisualLineEnd() {
updateCachedStateIfNeeded();
return myVisualLineEnd;
}
@NotNull
private VerticalInfo createVerticalInfo(LogicalPosition position) {
Document document = myEditor.getDocument();
int logicalLine = position.line;
if (logicalLine >= document.getLineCount()) {
logicalLine = Math.max(0, document.getLineCount() - 1);
}
int startOffset = document.getLineStartOffset(logicalLine);
int endOffset = document.getLineEndOffset(logicalLine);
// There is a possible case that active logical line is represented on multiple lines due to soft wraps processing.
// We want to highlight those visual lines as 'active' then, so, we calculate 'y' position for the logical line start
// and height in accordance with the number of occupied visual lines.
int visualLine = myEditor.offsetToVisualLine(document.getLineStartOffset(logicalLine));
int y = myEditor.visibleLineToY(visualLine);
int lineHeight = myEditor.getLineHeight();
int height = lineHeight;
List<? extends SoftWrap> softWraps = myEditor.getSoftWrapModel().getSoftWrapsForRange(startOffset, endOffset);
for (SoftWrap softWrap : softWraps) {
height += StringUtil.countNewLines(softWrap.getText()) * lineHeight;
}
return new VerticalInfo(y, height);
}
/**
* Recalculates caret visual position without changing its logical position (called when soft wraps are changing)
*/
void updateVisualPosition() {
updateCachedStateIfNeeded();
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition visUnawarePos = new LogicalPosition(myLogicalCaret.line, myLogicalCaret.column, myLogicalCaret.leansForward);
setCurrentLogicalCaret(visUnawarePos);
myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
updateVisualLineInfo();
myEditor.updateCaretCursor();
requestRepaint(oldInfo);
}
private void updateVisualLineInfo() {
myVisualLineStart = myEditor.logicalPositionToOffset(myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line, 0)));
myVisualLineEnd = myEditor.logicalPositionToOffset(myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line + 1, 0)));
}
void onInlayAdded(int offset) {
updateCachedStateIfNeeded();
int currentOffset = getOffset();
if (offset == currentOffset && myLogicalCaret.leansForward) {
VisualPosition pos = myEditor.offsetToVisualPosition(currentOffset, true, false);
moveToVisualPosition(pos);
}
else {
updateVisualPosition();
}
}
private void setCurrentLogicalCaret(@NotNull LogicalPosition position) {
myLogicalCaret = position;
myCaretInfo = createVerticalInfo(position);
}
int getWordAtCaretStart() {
Document document = myEditor.getDocument();
int offset = getOffset();
if (offset == 0) return 0;
int lineNumber = getLogicalPosition().line;
int newOffset = offset - 1;
int minOffset = lineNumber > 0 ? document.getLineEndOffset(lineNumber - 1) : 0;
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset > minOffset; newOffset--) {
if (EditorActionUtil.isWordOrLexemeStart(myEditor, newOffset, camel)) break;
}
return newOffset;
}
int getWordAtCaretEnd() {
Document document = myEditor.getDocument();
int offset = getOffset();
if (offset >= document.getTextLength() - 1 || document.getLineCount() == 0) return offset;
int newOffset = offset + 1;
int lineNumber = getLogicalPosition().line;
int maxOffset = document.getLineEndOffset(lineNumber);
if (newOffset > maxOffset) {
if (lineNumber + 1 >= document.getLineCount()) return offset;
maxOffset = document.getLineEndOffset(lineNumber + 1);
}
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset < maxOffset; newOffset++) {
if (EditorActionUtil.isWordOrLexemeEnd(myEditor, newOffset, camel)) break;
}
return newOffset;
}
private CaretImpl cloneWithoutSelection() {
updateCachedStateIfNeeded();
CaretImpl clone = new CaretImpl(myEditor);
clone.myLogicalCaret = myLogicalCaret;
clone.myCaretInfo = myCaretInfo;
clone.myVisibleCaret = myVisibleCaret;
clone.myPositionMarker = new PositionMarker(getOffset());
clone.myLeansTowardsLargerOffsets = myLeansTowardsLargerOffsets;
clone.myVirtualSpaceOffset = myVirtualSpaceOffset;
clone.myVisualLineStart = myVisualLineStart;
clone.myVisualLineEnd = myVisualLineEnd;
clone.mySkipChangeRequests = mySkipChangeRequests;
clone.myLastColumnNumber = myLastColumnNumber;
clone.myReportCaretMoves = myReportCaretMoves;
clone.myDesiredX = myDesiredX;
clone.myDesiredSelectionStartColumn = -1;
clone.myDesiredSelectionEndColumn = -1;
return clone;
}
@Nullable
@Override
public Caret clone(boolean above) {
assertIsDispatchThread();
int lineShift = above ? -1 : 1;
LogicalPosition oldPosition = getLogicalPosition();
int newLine = oldPosition.line + lineShift;
if (newLine < 0 || newLine >= myEditor.getDocument().getLineCount()) {
return null;
}
final CaretImpl clone = cloneWithoutSelection();
final int newSelectionStartOffset;
final int newSelectionEndOffset;
final int newSelectionStartColumn;
final int newSelectionEndColumn;
final VisualPosition newSelectionStartPosition;
final VisualPosition newSelectionEndPosition;
final boolean hasNewSelection;
if (hasSelection() || myDesiredSelectionStartColumn >=0 || myDesiredSelectionEndColumn >= 0) {
VisualPosition startPosition = getSelectionStartPosition();
VisualPosition endPosition = getSelectionEndPosition();
VisualPosition leadPosition = getLeadSelectionPosition();
boolean leadIsStart = leadPosition.equals(startPosition);
boolean leadIsEnd = leadPosition.equals(endPosition);
LogicalPosition selectionStart = myEditor.visualToLogicalPosition(leadIsStart || leadIsEnd ? leadPosition : startPosition);
LogicalPosition selectionEnd = myEditor.visualToLogicalPosition(leadIsEnd ? startPosition : endPosition);
newSelectionStartColumn = myDesiredSelectionStartColumn < 0 ? selectionStart.column : myDesiredSelectionStartColumn;
newSelectionEndColumn = myDesiredSelectionEndColumn < 0 ? selectionEnd.column : myDesiredSelectionEndColumn;
LogicalPosition newSelectionStart = truncate(selectionStart.line + lineShift, newSelectionStartColumn);
LogicalPosition newSelectionEnd = truncate(selectionEnd.line + lineShift, newSelectionEndColumn);
newSelectionStartOffset = myEditor.logicalPositionToOffset(newSelectionStart);
newSelectionEndOffset = myEditor.logicalPositionToOffset(newSelectionEnd);
newSelectionStartPosition = myEditor.logicalToVisualPosition(newSelectionStart);
newSelectionEndPosition = myEditor.logicalToVisualPosition(newSelectionEnd);
hasNewSelection = !newSelectionStart.equals(newSelectionEnd);
}
else {
newSelectionStartOffset = 0;
newSelectionEndOffset = 0;
newSelectionStartPosition = null;
newSelectionEndPosition = null;
hasNewSelection = false;
newSelectionStartColumn = -1;
newSelectionEndColumn = -1;
}
clone.moveToLogicalPosition(new LogicalPosition(newLine, myLastColumnNumber), false, null, false);
clone.myLastColumnNumber = myLastColumnNumber;
clone.myDesiredX = myDesiredX >= 0 ? myDesiredX : getCurrentX();
clone.myDesiredSelectionStartColumn = newSelectionStartColumn;
clone.myDesiredSelectionEndColumn = newSelectionEndColumn;
if (myEditor.getCaretModel().addCaret(clone, true)) {
if (hasNewSelection) {
myEditor.getCaretModel().doWithCaretMerging(
() -> clone.setSelection(newSelectionStartPosition, newSelectionStartOffset, newSelectionEndPosition, newSelectionEndOffset));
if (!clone.isValid()) {
return null;
}
}
myEditor.getScrollingModel().scrollTo(clone.getLogicalPosition(), ScrollType.RELATIVE);
return clone;
}
else {
Disposer.dispose(clone);
return null;
}
}
private LogicalPosition truncate(int line, int column) {
if (line < 0) {
return new LogicalPosition(0, 0);
}
else if (line >= myEditor.getDocument().getLineCount()) {
return myEditor.offsetToLogicalPosition(myEditor.getDocument().getTextLength());
}
else {
return new LogicalPosition(line, column);
}
}
/**
* @return information on whether current selection's direction in known
* @see #setUnknownDirection(boolean)
*/
boolean isUnknownDirection() {
return myUnknownDirection;
}
/**
* There is a possible case that we don't know selection's direction. For example, a user might triple-click editor (select the
* whole line). We can't say what selection end is a {@link #getLeadSelectionOffset() leading end} then. However, that matters
* in a situation when a user clicks before or after that line holding Shift key. It's expected that the selection is expanded
* up to that point than.
* <p/>
* That's why we allow to specify that the direction is unknown and {@link #isUnknownDirection() expose this information}
* later.
* <p/>
* <b>Note:</b> when this method is called with {@code 'true'}, subsequent calls are guaranteed to return {@code true}
* until selection is changed. 'Unknown direction' flag is automatically reset then.
*
*/
void setUnknownDirection(boolean unknownDirection) {
myUnknownDirection = unknownDirection;
}
@Override
public int getSelectionStart() {
validateContext(false);
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
if (marker != null) {
return marker.getStartOffset();
}
}
return getOffset();
}
@NotNull
@Override
public VisualPosition getSelectionStartPosition() {
validateContext(true);
VisualPosition position;
SelectionMarker marker = mySelectionMarker;
if (hasSelection()) {
position = getRangeMarkerStartPosition();
if (position == null) {
VisualPosition startPosition = myEditor.offsetToVisualPosition(marker.getStartOffset(), true, false);
VisualPosition endPosition = myEditor.offsetToVisualPosition(marker.getEndOffset(), false, true);
position = startPosition.after(endPosition) ? endPosition : startPosition;
}
}
else {
position = isVirtualSelectionEnabled() ? getVisualPosition() :
myEditor.offsetToVisualPosition(getOffset(), getLogicalPosition().leansForward, false);
}
if (hasVirtualSelection()) {
position = new VisualPosition(position.line, position.column + marker.startVirtualOffset);
}
return position;
}
LogicalPosition getSelectionStartLogicalPosition() {
validateContext(true);
LogicalPosition position;
SelectionMarker marker = mySelectionMarker;
if (hasSelection()) {
VisualPosition visualPosition = getRangeMarkerStartPosition();
position = visualPosition == null ? myEditor.offsetToLogicalPosition(marker.getStartOffset()).leanForward(true)
: myEditor.visualToLogicalPosition(visualPosition);
}
else {
position = getLogicalPosition();
}
if (hasVirtualSelection()) {
position = new LogicalPosition(position.line, position.column + marker.startVirtualOffset);
}
return position;
}
@Override
public int getSelectionEnd() {
validateContext(false);
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
if (marker != null) {
return marker.getEndOffset();
}
}
return getOffset();
}
@NotNull
@Override
public VisualPosition getSelectionEndPosition() {
validateContext(true);
VisualPosition position;
SelectionMarker marker = mySelectionMarker;
if (hasSelection()) {
position = getRangeMarkerEndPosition();
if (position == null) {
VisualPosition startPosition = myEditor.offsetToVisualPosition(marker.getStartOffset(), true, false);
VisualPosition endPosition = myEditor.offsetToVisualPosition(marker.getEndOffset(), false, true);
position = startPosition.after(endPosition) ? startPosition : endPosition;
}
}
else {
position = isVirtualSelectionEnabled() ? getVisualPosition() :
myEditor.offsetToVisualPosition(getOffset(), getLogicalPosition().leansForward, false);
}
if (hasVirtualSelection()) {
position = new VisualPosition(position.line, position.column + marker.endVirtualOffset);
}
return position;
}
LogicalPosition getSelectionEndLogicalPosition() {
validateContext(true);
LogicalPosition position;
SelectionMarker marker = mySelectionMarker;
if (hasSelection()) {
VisualPosition visualPosition = getRangeMarkerEndPosition();
position = visualPosition == null ? myEditor.offsetToLogicalPosition(marker.getEndOffset())
: myEditor.visualToLogicalPosition(visualPosition);
}
else {
position = getLogicalPosition();
}
if (hasVirtualSelection()) {
position = new LogicalPosition(position.line, position.column + marker.endVirtualOffset);
}
return position;
}
@Override
public boolean hasSelection() {
validateContext(false);
SelectionMarker marker = mySelectionMarker;
return marker != null && marker.isValid() && (marker.getEndOffset() > marker.getStartOffset()
|| isVirtualSelectionEnabled() && marker.hasVirtualSelection());
}
@Override
public void setSelection(int startOffset, int endOffset) {
setSelection(startOffset, endOffset, true);
}
@Override
public void setSelection(int startOffset, int endOffset, boolean updateSystemSelection) {
doSetSelection(myEditor.offsetToVisualPosition(startOffset, true, false), startOffset,
myEditor.offsetToVisualPosition(endOffset, false, true), endOffset,
false, updateSystemSelection);
}
@Override
public void setSelection(int startOffset, @Nullable VisualPosition endPosition, int endOffset) {
VisualPosition startPosition;
if (hasSelection()) {
startPosition = getLeadSelectionPosition();
}
else {
startPosition = myEditor.offsetToVisualPosition(startOffset, true, false);
}
setSelection(startPosition, startOffset, endPosition, endOffset);
}
@Override
public void setSelection(@Nullable VisualPosition startPosition, int startOffset, @Nullable VisualPosition endPosition, int endOffset) {
setSelection(startPosition, startOffset, endPosition, endOffset, true);
}
@Override
public void setSelection(@Nullable VisualPosition startPosition, int startOffset, @Nullable VisualPosition endPosition, int endOffset,
boolean updateSystemSelection) {
VisualPosition startPositionToUse = startPosition == null ? myEditor.offsetToVisualPosition(startOffset, true, false) : startPosition;
VisualPosition endPositionToUse = endPosition == null ? myEditor.offsetToVisualPosition(endOffset, false, true) : endPosition;
doSetSelection(startPositionToUse, startOffset, endPositionToUse, endOffset, true, updateSystemSelection);
}
private void doSetSelection(@NotNull final VisualPosition startPosition,
final int _startOffset,
@NotNull final VisualPosition endPosition,
final int _endOffset,
final boolean visualPositionAware,
final boolean updateSystemSelection)
{
myEditor.getCaretModel().doWithCaretMerging(() -> {
int startOffset = DocumentUtil.alignToCodePointBoundary(myEditor.getDocument(), _startOffset);
int endOffset = DocumentUtil.alignToCodePointBoundary(myEditor.getDocument(), _endOffset);
myUnknownDirection = false;
final Document doc = myEditor.getDocument();
validateContext(true);
int textLength = doc.getTextLength();
if (startOffset < 0 || startOffset > textLength) {
LOG.error("Wrong startOffset: " + startOffset + ", textLength=" + textLength);
}
if (endOffset < 0 || endOffset > textLength) {
LOG.error("Wrong endOffset: " + endOffset + ", textLength=" + textLength);
}
if (!visualPositionAware && startOffset == endOffset) {
removeSelection();
return;
}
/* Normalize selection */
boolean switchedOffsets = false;
if (startOffset > endOffset) {
int tmp = startOffset;
startOffset = endOffset;
endOffset = tmp;
switchedOffsets = true;
}
FoldingModelEx foldingModel = myEditor.getFoldingModel();
FoldRegion startFold = foldingModel.getCollapsedRegionAtOffset(startOffset);
if (startFold != null && startFold.getStartOffset() < startOffset) {
startOffset = startFold.getStartOffset();
}
FoldRegion endFold = foldingModel.getCollapsedRegionAtOffset(endOffset);
if (endFold != null && endFold.getStartOffset() < endOffset) {
// All visual positions that lay at collapsed fold region placeholder are mapped to the same offset. Hence, there are
// at least two distinct situations - selection end is located inside collapsed fold region placeholder and just before it.
// We want to expand selection to the fold region end at the former case and keep selection as-is at the latest one.
endOffset = endFold.getEndOffset();
}
int oldSelectionStart;
int oldSelectionEnd;
if (hasSelection()) {
oldSelectionStart = getSelectionStart();
oldSelectionEnd = getSelectionEnd();
if (oldSelectionStart == startOffset && oldSelectionEnd == endOffset && !visualPositionAware) return;
}
else {
oldSelectionStart = oldSelectionEnd = getOffset();
}
SelectionMarker marker = new SelectionMarker(startOffset, endOffset);
if (visualPositionAware) {
if (endPosition.after(startPosition)) {
setRangeMarkerStartPosition(startPosition);
setRangeMarkerEndPosition(endPosition);
setRangeMarkerEndPositionIsLead(false);
}
else {
setRangeMarkerStartPosition(endPosition);
setRangeMarkerEndPosition(startPosition);
setRangeMarkerEndPositionIsLead(true);
}
if (isVirtualSelectionEnabled() &&
myEditor.getDocument().getLineNumber(startOffset) == myEditor.getDocument().getLineNumber(endOffset)) {
int endLineColumn = myEditor.offsetToVisualPosition(endOffset).column;
int startDiff =
EditorUtil.isAtLineEnd(myEditor, switchedOffsets ? endOffset : startOffset) ? startPosition.column - endLineColumn : 0;
int endDiff =
EditorUtil.isAtLineEnd(myEditor, switchedOffsets ? startOffset : endOffset) ? endPosition.column - endLineColumn : 0;
marker.startVirtualOffset = Math.max(0, Math.min(startDiff, endDiff));
marker.endVirtualOffset = Math.max(0, Math.max(startDiff, endDiff));
}
}
mySelectionMarker = marker;
myEditor.getSelectionModel().fireSelectionChanged(oldSelectionStart, oldSelectionEnd, startOffset, endOffset);
if (updateSystemSelection) {
updateSystemSelection();
}
});
}
private void updateSystemSelection() {
if (GraphicsEnvironment.isHeadless()) return;
final Clipboard clip = myEditor.getComponent().getToolkit().getSystemSelection();
if (clip != null) {
clip.setContents(new StringSelection(myEditor.getSelectionModel().getSelectedText(true)), EmptyClipboardOwner.INSTANCE);
}
}
@Override
public void removeSelection() {
if (myEditor.isStickySelection()) {
// Most of our 'change caret position' actions (like move caret to word start/end etc) remove active selection.
// However, we don't want to do that for 'sticky selection'.
return;
}
myEditor.getCaretModel().doWithCaretMerging(() -> {
validateContext(true);
int caretOffset = getOffset();
RangeMarker marker = mySelectionMarker;
if (marker != null && marker.isValid()) {
int startOffset = marker.getStartOffset();
int endOffset = marker.getEndOffset();
mySelectionMarker = null;
myEditor.getSelectionModel().fireSelectionChanged(startOffset, endOffset, caretOffset, caretOffset);
}
});
}
@Override
public int getLeadSelectionOffset() {
validateContext(false);
int caretOffset = getOffset();
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
if (marker != null && marker.isValid()) {
int startOffset = marker.getStartOffset();
int endOffset = marker.getEndOffset();
if (caretOffset != startOffset && caretOffset != endOffset) {
// Try to check if current selection is tweaked by fold region.
FoldingModelEx foldingModel = myEditor.getFoldingModel();
FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(caretOffset);
if (foldRegion != null) {
if (foldRegion.getStartOffset() == startOffset) {
return endOffset;
}
else if (foldRegion.getEndOffset() == endOffset) {
return startOffset;
}
}
}
if (caretOffset == endOffset) {
return startOffset;
}
else {
return endOffset;
}
}
}
return caretOffset;
}
@NotNull
@Override
public VisualPosition getLeadSelectionPosition() {
SelectionMarker marker = mySelectionMarker;
VisualPosition caretPosition = getVisualPosition();
if (isVirtualSelectionEnabled() && !hasSelection()) {
return caretPosition;
}
if (marker == null || !marker.isValid()) {
return caretPosition;
}
if (isRangeMarkerEndPositionIsLead()) {
VisualPosition result = getRangeMarkerEndPosition();
if (result == null) {
return getSelectionEndPosition();
}
else {
if (hasVirtualSelection()) {
result = new VisualPosition(result.line, result.column + marker.endVirtualOffset);
}
return result;
}
}
else {
VisualPosition result = getRangeMarkerStartPosition();
if (result == null) {
return getSelectionStartPosition();
}
else {
if (hasVirtualSelection()) {
result = new VisualPosition(result.line, result.column + marker.startVirtualOffset);
}
return result;
}
}
}
@Override
public void selectLineAtCaret() {
validateContext(true);
myEditor.getCaretModel().doWithCaretMerging(() -> SelectionModelImpl.doSelectLineAtCaret(this));
}
@Override
public void selectWordAtCaret(final boolean honorCamelWordsSettings) {
validateContext(true);
myEditor.getCaretModel().doWithCaretMerging(() -> {
removeSelection();
final EditorSettings settings = myEditor.getSettings();
boolean camelTemp = settings.isCamelWords();
final boolean needOverrideSetting = camelTemp && !honorCamelWordsSettings;
if (needOverrideSetting) {
settings.setCamelWords(false);
}
try {
EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET);
handler.execute(myEditor, this, myEditor.getDataContext());
}
finally {
if (needOverrideSetting) {
settings.resetCamelWords();
}
}
});
}
@Nullable
@Override
public String getSelectedText() {
if (!hasSelection()) {
return null;
}
SelectionMarker selectionMarker = mySelectionMarker;
CharSequence text = myEditor.getDocument().getCharsSequence();
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
String selectedText = text.subSequence(selectionStart, selectionEnd).toString();
if (isVirtualSelectionEnabled() && selectionMarker.hasVirtualSelection()) {
int padding = selectionMarker.endVirtualOffset - selectionMarker.startVirtualOffset;
StringBuilder builder = new StringBuilder(selectedText.length() + padding);
builder.append(selectedText);
for (int i = 0; i < padding; i++) {
builder.append(' ');
}
return builder.toString();
}
else {
return selectedText;
}
}
private static void validateContext(boolean requireEdt) {
if (requireEdt) {
ApplicationManager.getApplication().assertIsDispatchThread();
}
else {
ApplicationManager.getApplication().assertReadAccessAllowed();
}
}
private boolean isVirtualSelectionEnabled() {
return myEditor.isColumnMode();
}
boolean hasVirtualSelection() {
validateContext(false);
SelectionMarker marker = mySelectionMarker;
return marker != null && marker.isValid() && isVirtualSelectionEnabled() && marker.hasVirtualSelection();
}
void resetVirtualSelection() {
SelectionMarker marker = mySelectionMarker;
if (marker != null) marker.resetVirtualSelection();
}
private int getCurrentX() {
return myEditor.visualPositionToXY(myVisibleCaret).x;
}
@Override
@NotNull
public EditorImpl getEditor() {
return myEditor;
}
@Override
public String toString() {
return "Caret at " + (myDocumentUpdateCounter == myEditor.getCaretModel().myDocumentUpdateCounter ? myVisibleCaret : getOffset()) +
(mySelectionMarker == null ? "" : ", selection marker: " + mySelectionMarker);
}
@Override
public boolean isAtRtlLocation() {
return myEditor.myView.isRtlLocation(getVisualPosition());
}
@Override
public boolean isAtBidiRunBoundary() {
return myEditor.myView.isAtBidiRunBoundary(getVisualPosition());
}
@NotNull
@Override
public CaretVisualAttributes getVisualAttributes() {
CaretVisualAttributes attrs = getUserData(VISUAL_ATTRIBUTES_KEY);
return attrs == null ? CaretVisualAttributes.DEFAULT : attrs;
}
@Override
public void setVisualAttributes(@NotNull CaretVisualAttributes attributes) {
putUserData(VISUAL_ATTRIBUTES_KEY, attributes == CaretVisualAttributes.DEFAULT ? null : attributes);
requestRepaint(myCaretInfo);
}
@NotNull
@Override
public String dumpState() {
return "{valid: " + isValid +
", update counter: " + myDocumentUpdateCounter +
", position: " + myPositionMarker +
", logical pos: " + myLogicalCaret +
", visual pos: " + myVisibleCaret +
", visual line start: " + myVisualLineStart +
", visual line end: " + myVisualLineEnd +
", skip change requests: " + mySkipChangeRequests +
", desired selection start column: " + myDesiredSelectionStartColumn +
", desired selection end column: " + myDesiredSelectionEndColumn +
", report caret moves: " + myReportCaretMoves +
", desired x: " + myDesiredX +
", selection marker: " + mySelectionMarker +
", rangeMarker start position: " + myRangeMarkerStartPosition +
", rangeMarker end position: " + myRangeMarkerEndPosition +
", rangeMarker end position is lead: " + myRangeMarkerEndPositionIsLead +
", unknown direction: " + myUnknownDirection +
", virtual space offset: " + myVirtualSpaceOffset + '}';
}
/**
* Encapsulates information about target vertical range info - its {@code 'y'} coordinate and height in pixels.
*/
private static class VerticalInfo {
public final int y;
public final int height;
private VerticalInfo(int y, int height) {
this.y = y;
this.height = height;
}
}
@Nullable
private VisualPosition getRangeMarkerStartPosition() {
invalidateRangeMarkerVisualPositions(mySelectionMarker);
return myRangeMarkerStartPosition;
}
private void setRangeMarkerStartPosition(@NotNull VisualPosition startPosition) {
myRangeMarkerStartPosition = startPosition;
}
@Nullable
private VisualPosition getRangeMarkerEndPosition() {
invalidateRangeMarkerVisualPositions(mySelectionMarker);
return myRangeMarkerEndPosition;
}
private void setRangeMarkerEndPosition(@NotNull VisualPosition endPosition) {
myRangeMarkerEndPosition = endPosition;
}
private boolean isRangeMarkerEndPositionIsLead() {
return myRangeMarkerEndPositionIsLead;
}
private void setRangeMarkerEndPositionIsLead(boolean endPositionIsLead) {
myRangeMarkerEndPositionIsLead = endPositionIsLead;
}
private void invalidateRangeMarkerVisualPositions(RangeMarker marker) {
SoftWrapModelImpl model = myEditor.getSoftWrapModel();
InlayModelImpl inlayModel = myEditor.getInlayModel();
int startOffset = marker.getStartOffset();
int endOffset = marker.getEndOffset();
if (!myEditor.offsetToVisualPosition(startOffset, true, false).equals(myRangeMarkerStartPosition) &&
model.getSoftWrap(startOffset) == null && !inlayModel.hasInlineElementAt(startOffset) ||
!myEditor.offsetToVisualPosition(endOffset, false, true).equals(myRangeMarkerEndPosition)
&& model.getSoftWrap(endOffset) == null && !inlayModel.hasInlineElementAt(endOffset)) {
myRangeMarkerStartPosition = null;
myRangeMarkerEndPosition = null;
}
}
void updateCachedStateIfNeeded() {
if (!ApplicationManager.getApplication().isDispatchThread()) return;
int modelCounter = myEditor.getCaretModel().myDocumentUpdateCounter;
if (myDocumentUpdateCounter != modelCounter) {
LogicalPosition lp = myEditor.offsetToLogicalPosition(getOffset());
setCurrentLogicalCaret(new LogicalPosition(lp.line, lp.column + myVirtualSpaceOffset, myLeansTowardsLargerOffsets));
myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
updateVisualLineInfo();
setLastColumnNumber(myLogicalCaret.column);
myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
myDesiredX = -1;
myDocumentUpdateCounter = modelCounter;
}
}
@TestOnly
public void validateState() {
LOG.assertTrue(!DocumentUtil.isInsideSurrogatePair(myEditor.getDocument(), getOffset()));
LOG.assertTrue(!DocumentUtil.isInsideSurrogatePair(myEditor.getDocument(), getSelectionStart()));
LOG.assertTrue(!DocumentUtil.isInsideSurrogatePair(myEditor.getDocument(), getSelectionEnd()));
}
class PositionMarker extends RangeMarkerImpl {
private PositionMarker(int offset) {
super(myEditor.getDocument(), offset, offset, false);
myEditor.getCaretModel().myPositionMarkerTree.addInterval(this, offset, offset, false, false, 0);
}
@Override
public void dispose() {
if (isValid()) {
myEditor.getCaretModel().myPositionMarkerTree.removeInterval(this);
}
}
@Override
protected void changedUpdateImpl(@NotNull DocumentEvent e) {
int oldOffset = intervalStart();
super.changedUpdateImpl(e);
if (isValid()) {
// Under certain conditions, when text is inserted at caret position, we position caret at the end of inserted text.
// Ideally, client code should be responsible for positioning caret after document modification, but in case of
// postponed formatting (after PSI modifications), this is hard to implement, so a heuristic below is used.
if (e.getOldLength() == 0 && oldOffset == e.getOffset() &&
!Boolean.TRUE.equals(myEditor.getUserData(EditorImpl.DISABLE_CARET_SHIFT_ON_WHITESPACE_INSERTION)) &&
needToShiftWhiteSpaces(e)) {
int afterInserted = e.getOffset() + e.getNewLength();
setIntervalStart(afterInserted);
setIntervalEnd(afterInserted);
}
int offset = intervalStart();
if (DocumentUtil.isInsideSurrogatePair(getDocument(), offset)) {
setIntervalStart(offset - 1);
setIntervalEnd(offset - 1);
}
}
else {
setValid(true);
int newOffset = Math.min(intervalStart(), e.getOffset() + e.getNewLength());
if (!((DocumentEx)e.getDocument()).isInBulkUpdate() && e.isWholeTextReplaced()) {
try {
final int line = ((DocumentEventImpl)e).translateLineViaDiff(myLogicalCaret.line);
newOffset = myEditor.logicalPositionToOffset(new LogicalPosition(line, myLogicalCaret.column));
}
catch (FilesTooBigForDiffException ex) {
LOG.info(ex);
}
}
newOffset = DocumentUtil.alignToCodePointBoundary(getDocument(), newOffset);
setIntervalStart(newOffset);
setIntervalEnd(newOffset);
}
if (oldOffset >= e.getOffset() && oldOffset <= e.getOffset() + e.getOldLength() && e.getNewLength() == 0 &&
myEditor.getInlayModel().hasInlineElementAt(e.getOffset())) {
myLeansTowardsLargerOffsets = true;
}
else if (oldOffset == e.getOffset()) {
myLeansTowardsLargerOffsets = false;
}
}
private boolean needToShiftWhiteSpaces(final DocumentEvent e) {
return e.getOffset() > 0 && Character.isWhitespace(e.getDocument().getImmutableCharSequence().charAt(e.getOffset() - 1)) &&
CharArrayUtil.containsOnlyWhiteSpaces(e.getNewFragment()) && !CharArrayUtil.containLineBreaks(e.getNewFragment());
}
@Override
protected void onReTarget(int startOffset, int endOffset, int destOffset) {
int offset = intervalStart();
if (DocumentUtil.isInsideSurrogatePair(getDocument(), offset)) {
setIntervalStart(offset - 1);
setIntervalEnd(offset - 1);
}
}
}
class SelectionMarker extends RangeMarkerImpl {
// offsets of selection start/end position relative to end of line - can be non-zero in column selection mode
// these are non-negative values, myStartVirtualOffset is always less or equal to myEndVirtualOffset
private int startVirtualOffset;
private int endVirtualOffset;
private SelectionMarker(int start, int end) {
super(myEditor.getDocument(), start, end, false);
myEditor.getCaretModel().mySelectionMarkerTree.addInterval(this, start, end, false, false, 0);
}
private void resetVirtualSelection() {
startVirtualOffset = 0;
endVirtualOffset = 0;
}
private boolean hasVirtualSelection() {
return endVirtualOffset > startVirtualOffset;
}
@Override
public void dispose() {
if (isValid()) {
myEditor.getCaretModel().mySelectionMarkerTree.removeInterval(this);
}
}
@Override
protected void changedUpdateImpl(@NotNull DocumentEvent e) {
super.changedUpdateImpl(e);
if (isValid()) {
int startOffset = intervalStart();
int endOffset = intervalEnd();
if (DocumentUtil.isInsideSurrogatePair(getDocument(), startOffset)) setIntervalStart(startOffset - 1);
if (DocumentUtil.isInsideSurrogatePair(getDocument(), endOffset)) setIntervalStart(endOffset - 1);
}
if (endVirtualOffset > 0 && isValid()) {
Document document = e.getDocument();
int startAfter = intervalStart();
int endAfter = intervalEnd();
if (!DocumentUtil.isAtLineEnd(endAfter, document) || document.getLineNumber(startAfter) != document.getLineNumber(endAfter)) {
resetVirtualSelection();
}
}
}
@Override
protected void onReTarget(int startOffset, int endOffset, int destOffset) {
int start = intervalStart();
if (DocumentUtil.isInsideSurrogatePair(getDocument(), start)) {
setIntervalStart(start - 1);
}
int end = intervalEnd();
if (DocumentUtil.isInsideSurrogatePair(getDocument(), end)) {
setIntervalStart(end - 1);
}
}
@Override
public String toString() {
return super.toString() + (hasVirtualSelection() ? " virtual selection: " + startVirtualOffset + "-" + endVirtualOffset : "");
}
}
}