blob: e2693b2c835fec1bfed3375b1c1ed2eee5477bd3 [file] [log] [blame]
/*
* Copyright 2000-2014 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.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.UserDataHolderBase;
import com.intellij.openapi.util.text.StringUtil;
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 java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
public class CaretImpl extends UserDataHolderBase implements Caret {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.CaretImpl");
private final EditorImpl myEditor;
private boolean isValid = true;
private LogicalPosition myLogicalCaret;
private VerticalInfo myCaretInfo;
private VisualPosition myVisibleCaret;
private int myOffset;
private int myVirtualSpaceOffset;
private int myVisualLineStart;
private int myVisualLineEnd;
private RangeMarker savedBeforeBulkCaretMarker;
private boolean mySkipChangeRequests;
/**
* Initial horizontal caret position during vertical navigation.
* Similar to {@link #myDesiredX}, but represents logical caret position (<code>getLogicalPosition().column</code>) rather than visual.
*/
private int myLastColumnNumber = 0;
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 MyRangeMarker mySelectionMarker;
private int startBefore;
private int endBefore;
boolean myUnknownDirection;
// 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 myStartVirtualOffset;
private int myEndVirtualOffset;
CaretImpl(EditorImpl editor) {
myEditor = editor;
myLogicalCaret = new LogicalPosition(0, 0);
myVisibleCaret = new VisualPosition(0, 0);
myCaretInfo = new VerticalInfo(0, 0);
myOffset = 0;
myVisualLineStart = 0;
Document doc = myEditor.getDocument();
myVisualLineEnd = doc.getLineCount() > 1 ? doc.getLineStartOffset(1) : doc.getLineCount() == 0 ? 0 : doc.getLineEndOffset(0);
}
void onBulkDocumentUpdateStarted(@NotNull Document doc) {
if (doc != myEditor.getDocument() || myOffset > doc.getTextLength() || savedBeforeBulkCaretMarker != null) return;
savedBeforeBulkCaretMarker = doc.createRangeMarker(myOffset, myOffset);
}
void onBulkDocumentUpdateFinished(@NotNull Document doc) {
if (doc != myEditor.getDocument() || myEditor.getCaretModel().myIsInUpdate) return;
LOG.assertTrue(!myReportCaretMoves);
if (savedBeforeBulkCaretMarker != null) {
if(savedBeforeBulkCaretMarker.isValid()) {
if(savedBeforeBulkCaretMarker.getStartOffset() != myOffset) {
moveToOffset(savedBeforeBulkCaretMarker.getStartOffset());
}
} else if (myOffset > doc.getTextLength()) {
moveToOffset(doc.getTextLength());
}
releaseBulkCaretMarker();
}
}
public void beforeDocumentChange() {
MyRangeMarker marker = mySelectionMarker;
if (marker != null && marker.isValid()) {
startBefore = marker.getStartOffset();
endBefore = marker.getEndOffset();
}
}
public void documentChanged() {
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
int endAfter;
int startAfter;
if (marker.isValid()) {
startAfter = marker.getStartOffset();
endAfter = marker.getEndOffset();
if (myEndVirtualOffset > 0 && (!isVirtualSelectionEnabled()
|| !EditorUtil.isAtLineEnd(myEditor, endAfter)
|| myEditor.getDocument().getLineNumber(startAfter) != myEditor.getDocument().getLineNumber(endAfter))) {
myStartVirtualOffset = 0;
myEndVirtualOffset = 0;
}
}
else {
startAfter = endAfter = getOffset();
marker.release();
myStartVirtualOffset = 0;
myEndVirtualOffset = 0;
mySelectionMarker = null;
}
if (startBefore != startAfter || endBefore != endAfter) {
myEditor.getSelectionModel().fireSelectionChanged(startBefore, endBefore, startAfter, endAfter);
}
}
}
@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(new Runnable() {
public void run() {
final LogicalPosition logicalPosition = myEditor.offsetToLogicalPosition(offset);
CaretEvent event = moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, null, false);
final LogicalPosition positionByOffsetAfterMove = myEditor.offsetToLogicalPosition(myOffset);
if (!positionByOffsetAfterMove.equals(logicalPosition)) {
StringBuilder debugBuffer = new StringBuilder();
moveToLogicalPosition(logicalPosition, locateBeforeSoftWrap, debugBuffer, true);
int textStart = Math.max(0, Math.min(offset, myOffset) - 1);
final DocumentEx document = myEditor.getDocument();
int textEnd = Math.min(document.getTextLength() - 1, Math.max(offset, myOffset) + 1);
CharSequence text = document.getCharsSequence().subSequence(textStart, textEnd);
StringBuilder positionToOffsetTrace = new StringBuilder();
int inverseOffset = myEditor.logicalPositionToOffset(logicalPosition, positionToOffsetTrace);
LogMessageEx.error(
LOG, "caret moved to wrong offset. Please submit a dedicated ticket and attach current editor's text to it.",
String.format(
"Requested: offset=%d, logical position='%s' but actual: offset=%d, logical position='%s' (%s). %s%n"
+ "interested text [%d;%d): '%s'%n debug trace: %s%nLogical position -> offset ('%s'->'%d') trace: %s",
offset, logicalPosition, myOffset, myLogicalCaret, positionByOffsetAfterMove, myEditor.dumpState(),
textStart, textEnd, text, debugBuffer, logicalPosition, inverseOffset, positionToOffsetTrace
)
);
}
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(int columnShift, int lineShift, boolean withSelection, boolean scrollToCaret) {
moveCaretRelatively(columnShift, lineShift, withSelection, false, scrollToCaret);
}
void moveCaretRelatively(final int columnShift,
final int lineShift,
final boolean withSelection,
final boolean blockSelection,
final boolean scrollToCaret) {
assertIsDispatchThread();
if (mySkipChangeRequests) {
return;
}
if (myReportCaretMoves) {
LogMessageEx.error(LOG, "Unexpected caret move request");
}
if (!myEditor.isStickySelection() && !myEditor.getCaretModel().isDocumentChanged) {
CopyPasteManager.getInstance().stopKillRings();
}
myEditor.getCaretModel().doWithCaretMerging(new Runnable() {
public void run() {
SelectionModelImpl selectionModel = myEditor.getSelectionModel();
final int leadSelectionOffset = getLeadSelectionOffset();
final VisualPosition leadSelectionPosition = getLeadSelectionPosition();
LogicalPosition blockSelectionStart = selectionModel.hasBlockSelection()
? selectionModel.getBlockStart()
: getLogicalPosition();
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;
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 (EditorModificationUtil.calcAfterLineEnd(myEditor) >= 0 &&
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;
desiredX = -1;
lastColumnNumber = -1;
}
VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber);
if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) {
LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber));
int offset = myEditor.logicalPositionToOffset(log);
if (offset >= document.getTextLength()) {
int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength()).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;
desiredX = -1;
lastColumnNumber = -1;
}
}
if (!editorSettings.isCaretInsideTabs()) {
CharSequence text = document.getCharsSequence();
if (offset >= 0 && offset < document.getTextLength()) {
if (text.charAt(offset) == '\t' && (columnShift <= 0 || offset == myOffset)) {
if (columnShift <= 0) {
newColumnNumber = myEditor.offsetToVisualPosition(offset).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);
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 (blockSelection && !supportsMultipleCarets()) {
selectionModel.setBlockSelection(blockSelectionStart, getLogicalPosition());
}
else {
if (selectToDocumentStart) {
if (supportsMultipleCarets()) {
setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(0), 0);
}
else {
setSelection(leadSelectionOffset, 0);
}
}
else if (pos.line >= myEditor.getVisibleLineCount()) {
int endOffset = document.getTextLength();
if (leadSelectionOffset < endOffset) {
if (supportsMultipleCarets()) {
setSelection(leadSelectionPosition, leadSelectionOffset, myEditor.offsetToVisualPosition(endOffset), endOffset);
}
else {
setSelection(leadSelectionOffset, endOffset);
}
}
}
else {
int selectionStartToUse = leadSelectionOffset;
VisualPosition selectionStartPositionToUse = leadSelectionPosition;
if (isUnknownDirection()) {
if (getOffset() > leadSelectionOffset ^ getSelectionStart() < getSelectionEnd()) {
selectionStartToUse = getSelectionEnd();
selectionStartPositionToUse = getSelectionEndPosition();
}
else {
selectionStartToUse = getSelectionStart();
selectionStartPositionToUse = getSelectionStartPosition();
}
}
if (supportsMultipleCarets()) {
setSelection(selectionStartPositionToUse, selectionStartToUse, getVisualPosition(), getOffset());
}
else {
setSelection(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(new Runnable() {
public void run() {
moveToLogicalPosition(pos, false, null, true);
}
});
}
private CaretEvent doMoveToLogicalPosition(@NotNull LogicalPosition pos,
boolean locateBeforeSoftWrap,
@NonNls @Nullable StringBuilder debugBuffer,
boolean fireListeners) {
assertIsDispatchThread();
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;
Document doc = myEditor.getDocument();
if (column < 0) {
if (debugBuffer != null) {
debugBuffer.append("Resetting target logical column to zero as it is negative (").append(column).append(")\n");
}
column = 0;
softWrapColumns = 0;
}
if (line < 0) {
if (debugBuffer != null) {
debugBuffer.append("Resetting target logical line to zero as it is negative (").append(line).append(")\n");
}
line = 0;
softWrapLinesBefore = 0;
softWrapLinesCurrent = 0;
}
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 && !myEditor.getSelectionModel().hasBlockSelection()) {
int lineEndOffset = doc.getLineEndOffset(line);
final LogicalPosition endLinePosition = myEditor.offsetToLogicalPosition(lineEndOffset);
int lineEndColumnNumber = endLinePosition.column;
if (column > lineEndColumnNumber) {
int oldColumn = column;
column = lineEndColumnNumber;
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();
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition oldCaretPosition = myLogicalCaret;
LogicalPosition logicalPositionToUse;
if (pos.visualPositionAware) {
logicalPositionToUse = new LogicalPosition(
line, column, softWrapLinesBefore, softWrapLinesCurrent, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff
);
}
else {
logicalPositionToUse = new LogicalPosition(line, column);
}
setCurrentLogicalCaret(logicalPositionToUse);
final int offset = myEditor.logicalPositionToOffset(myLogicalCaret);
if (debugBuffer != null) {
debugBuffer.append("Resulting logical position to use: ").append(myLogicalCaret).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 = new Runnable() {
@Override
public void run() {
FoldRegion[] allCollapsedAt = myEditor.getFoldingModel().fetchCollapsedAt(offset);
for (FoldRegion foldRange : allCollapsedAt) {
foldRange.setExpanded(true);
}
}
};
mySkipChangeRequests = true;
try {
myEditor.getFoldingModel().runBatchFoldingOperation(runnable, false);
}
finally {
mySkipChangeRequests = false;
}
}
setLastColumnNumber(myLogicalCaret.column);
myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
myVisibleCaret = myEditor.logicalToVisualPosition(myLogicalCaret);
updateOffsetsFromLogicalPosition();
if (debugBuffer != null) {
debugBuffer.append("Storing offset ").append(myOffset).append(" (mapped from logical position ").append(myLogicalCaret).append(")\n");
}
LOG.assertTrue(myOffset >= 0 && myOffset <= myEditor.getDocument().getTextLength());
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 == myOffset) {
boolean restore = myReportCaretMoves;
myReportCaretMoves = false;
try {
moveToVisualPosition(visualPosition);
return null;
}
finally {
myReportCaretMoves = restore;
}
}
else {
LogMessageEx.error(LOG, "Invalid editor dimension mapping", String.format(
"Expected to map visual position '%s' to offset %d but got the following: -> logical position '%s'; -> offset %d. "
+ "State: %s", visualPosition, myOffset, logicalPosition, tmpOffset, myEditor.dumpState()
));
}
}
}
if (!oldCaretPosition.toVisualPosition().equals(myLogicalCaret.toVisualPosition())) {
CaretEvent event = new CaretEvent(myEditor, supportsMultipleCarets() ? this : null, oldCaretPosition, myLogicalCaret);
if (fireListeners) {
myEditor.getCaretModel().fireCaretPositionChanged(event);
}
else {
return event;
}
}
return null;
}
private boolean supportsMultipleCarets() {
return myEditor.getCaretModel().supportsMultipleCarets();
}
private void updateOffsetsFromLogicalPosition() {
myOffset = myEditor.logicalPositionToOffset(myLogicalCaret);
myVirtualSpaceOffset = myLogicalCaret.column - myEditor.offsetToLogicalPosition(myOffset).column;
}
private void setLastColumnNumber(int lastColumnNumber) {
myLastColumnNumber = lastColumnNumber;
myEditor.setLastColumnNumber(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(new Runnable() {
public void run() {
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.getCaretModel().isDocumentChanged && !pos.equals(myVisibleCaret)) {
CopyPasteManager.getInstance().stopKillRings();
}
myDesiredX = -1;
int column = pos.column;
int line = pos.line;
if (column < 0) column = 0;
if (line < 0) line = 0;
int lastLine = myEditor.getVisibleLineCount() - 1;
if (lastLine <= 0) {
lastLine = 0;
}
if (line > lastLine) {
line = lastLine;
}
EditorSettings editorSettings = myEditor.getSettings();
if (!editorSettings.isVirtualSpace() && line <= lastLine) {
int lineEndColumn = EditorUtil.getLastVisualLineColumnNumber(myEditor, line);
if (column > lineEndColumn) {
column = lineEndColumn;
}
if (column < 0 && line > 0) {
line--;
column = EditorUtil.getLastVisualLineColumnNumber(myEditor, line);
}
}
myVisibleCaret = new VisualPosition(line, column);
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition oldPosition = myLogicalCaret;
setCurrentLogicalCaret(myEditor.visualToLogicalPosition(myVisibleCaret));
updateOffsetsFromLogicalPosition();
LOG.assertTrue(myOffset >= 0 && myOffset <= myEditor.getDocument().getTextLength());
updateVisualLineInfo();
myEditor.getFoldingModel().flushCaretPosition();
setLastColumnNumber(myLogicalCaret.column);
myDesiredSelectionStartColumn = myDesiredSelectionEndColumn = -1;
myEditor.updateCaretCursor();
requestRepaint(oldInfo);
if (fireListeners && !oldPosition.equals(myLogicalCaret)) {
CaretEvent event = new CaretEvent(myEditor, supportsMultipleCarets() ? this : null, 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.getCaretModel().isDocumentChanged && !pos.equals(myLogicalCaret)) {
CopyPasteManager.getInstance().stopKillRings();
}
myReportCaretMoves = true;
try {
return doMoveToLogicalPosition(pos, locateBeforeSoftWrap, debugBuffer, fireListeners);
}
finally {
myReportCaretMoves = false;
}
}
private void assertIsDispatchThread() {
myEditor.assertIsDispatchThread();
}
private void validateCallContext() {
LOG.assertTrue(!myEditor.getCaretModel().myIsInUpdate, "Caret model is in its update process. All requests are illegal at this point.");
}
private void releaseBulkCaretMarker() {
if (savedBeforeBulkCaretMarker != null) {
savedBeforeBulkCaretMarker.dispose();
savedBeforeBulkCaretMarker = null;
}
}
@Override
public void dispose() {
if (mySelectionMarker != null) {
mySelectionMarker.release();
mySelectionMarker = null;
}
releaseBulkCaretMarker();
isValid = false;
}
@Override
public boolean isUpToDate() {
return !myEditor.getCaretModel().myIsInUpdate && !myReportCaretMoves;
}
@NotNull
@Override
public LogicalPosition getLogicalPosition() {
validateCallContext();
return myLogicalCaret;
}
@NotNull
@Override
public VisualPosition getVisualPosition() {
validateCallContext();
return myVisibleCaret;
}
@Override
public int getOffset() {
validateCallContext();
return myOffset;
}
@Override
public int getVisualLineStart() {
return myVisualLineStart;
}
@Override
public int getVisualLineEnd() {
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.
VisualPosition visualPosition = myEditor.offsetToVisualPosition(document.getLineStartOffset(logicalLine));
int y = myEditor.visualPositionToXY(visualPosition).y;
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)
*/
public void updateVisualPosition() {
VerticalInfo oldInfo = myCaretInfo;
LogicalPosition visUnawarePos = new LogicalPosition(myLogicalCaret.line, myLogicalCaret.column);
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 updateCaretPosition(@NotNull final DocumentEventImpl event) {
final DocumentEx document = myEditor.getDocument();
boolean performSoftWrapAdjustment = event.getNewLength() > 0 // We want to put caret just after the last added symbol
// There is a possible case that the user removes text just before the soft wrap. We want to keep caret
// on a visual line with soft wrap start then.
|| myEditor.getSoftWrapModel().getSoftWrap(event.getOffset()) != null;
if (event.isWholeTextReplaced()) {
int newLength = document.getTextLength();
if (myOffset == newLength - event.getNewLength() + event.getOldLength() || newLength == 0) {
moveToOffset(newLength, performSoftWrapAdjustment);
}
else {
try {
final int line = event.translateLineViaDiff(myLogicalCaret.line);
moveToLogicalPosition(new LogicalPosition(line, myLogicalCaret.column), performSoftWrapAdjustment, null, true);
}
catch (FilesTooBigForDiffException e1) {
LOG.info(e1);
moveToOffset(0);
}
}
}
else {
if (document.isInBulkUpdate()) return;
int startOffset = event.getOffset();
int oldEndOffset = startOffset + event.getOldLength();
int newOffset = myOffset;
if (myOffset > oldEndOffset || myOffset == oldEndOffset && needToShiftWhiteSpaces(event)) {
newOffset += event.getNewLength() - event.getOldLength();
}
else if (myOffset >= startOffset && myOffset <= oldEndOffset) {
newOffset = Math.min(newOffset, startOffset + event.getNewLength());
}
newOffset = Math.min(newOffset, document.getTextLength());
if (supportsMultipleCarets() && myOffset != startOffset) {
LogicalPosition pos = myEditor.offsetToLogicalPosition(newOffset);
moveToLogicalPosition(new LogicalPosition(pos.line, pos.column + myVirtualSpaceOffset), // retain caret in the virtual space
performSoftWrapAdjustment, null, true);
}
else {
moveToOffset(newOffset, performSoftWrapAdjustment);
}
}
updateVisualLineInfo();
}
private boolean needToShiftWhiteSpaces(final DocumentEvent e) {
if (!CharArrayUtil.containsOnlyWhiteSpaces(e.getNewFragment()) || CharArrayUtil.containLineBreaks(e.getNewFragment()))
return e.getOldLength() > 0;
if (e.getOffset() == 0) return false;
final char charBefore = myEditor.getDocument().getCharsSequence().charAt(e.getOffset() - 1);
//final char charAfter = myEditor.getDocument().getCharsSequence().charAt(e.getOffset() + e.getNewLength());
return Character.isWhitespace(charBefore)/* || !Character.isWhitespace(charAfter)*/;
}
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;
CharSequence text = document.getCharsSequence();
int newOffset = offset - 1;
int minOffset = lineNumber > 0 ? document.getLineEndOffset(lineNumber - 1) : 0;
boolean camel = myEditor.getSettings().isCamelWords();
for (; newOffset > minOffset; newOffset--) {
if (EditorActionUtil.isWordStart(text, newOffset, camel)) break;
}
return newOffset;
}
int getWordAtCaretEnd() {
Document document = myEditor.getDocument();
int offset = getOffset();
CharSequence text = document.getCharsSequence();
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.isWordEnd(text, newOffset, camel)) break;
}
return newOffset;
}
CaretImpl cloneWithoutSelection() {
CaretImpl clone = new CaretImpl(myEditor);
clone.myLogicalCaret = this.myLogicalCaret;
clone.myCaretInfo = this.myCaretInfo;
clone.myVisibleCaret = this.myVisibleCaret;
clone.myOffset = this.myOffset;
clone.myVirtualSpaceOffset = this.myVirtualSpaceOffset;
clone.myVisualLineStart = this.myVisualLineStart;
clone.myVisualLineEnd = this.myVisualLineEnd;
clone.savedBeforeBulkCaretMarker = this.savedBeforeBulkCaretMarker;
clone.mySkipChangeRequests = this.mySkipChangeRequests;
clone.myLastColumnNumber = this.myLastColumnNumber;
clone.myReportCaretMoves = this.myReportCaretMoves;
clone.myDesiredX = this.myDesiredX;
clone.myDesiredSelectionStartColumn = -1;
clone.myDesiredSelectionEndColumn = -1;
return clone;
}
@Nullable
@Override
public Caret clone(boolean above) {
assertIsDispatchThread();
int lineShift = above ? -1 : 1;
final CaretImpl clone = cloneWithoutSelection();
final int newSelectionStartOffset, newSelectionEndOffset, newSelectionStartColumn, newSelectionEndColumn;
final VisualPosition newSelectionStartPosition, 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;
}
LogicalPosition oldPosition = getLogicalPosition();
int newLine = oldPosition.line + lineShift;
if (newLine < 0 || newLine >= myEditor.getDocument().getLineCount()) {
Disposer.dispose(clone);
return null;
}
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)) {
if (hasNewSelection) {
myEditor.getCaretModel().doWithCaretMerging(new Runnable() {
@Override
public void run() {
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)
*/
public 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'</code>, subsequent calls are guaranteed to return <code>'true'</code>
* until selection is changed. 'Unknown direction' flag is automatically reset then.
*
* @param unknownDirection
*/
public void setUnknownDirection(boolean unknownDirection) {
myUnknownDirection = unknownDirection;
}
@Override
public int getSelectionStart() {
validateContext(false);
if (hasSelection()) {
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
return marker.getStartOffset();
}
}
return getOffset();
}
@NotNull
@Override
public VisualPosition getSelectionStartPosition() {
validateContext(false);
VisualPosition position;
if (hasSelection() && mySelectionMarker != null) {
position = mySelectionMarker.getStartPosition();
if (position == null) {
position = myEditor.offsetToVisualPosition(mySelectionMarker.getStartOffset());
}
}
else {
position = isVirtualSelectionEnabled() ? getVisualPosition() : myEditor.offsetToVisualPosition(getOffset());
}
if (hasVirtualSelection()) {
position = new VisualPosition(position.line, position.column + myStartVirtualOffset);
}
return position;
}
@Override
public int getSelectionEnd() {
validateContext(false);
if (hasSelection()) {
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
return marker.getEndOffset();
}
}
return getOffset();
}
@NotNull
@Override
public VisualPosition getSelectionEndPosition() {
validateContext(false);
VisualPosition position;
if (hasSelection() && mySelectionMarker != null) {
position = mySelectionMarker.getEndPosition();
if (position == null) {
position = myEditor.offsetToVisualPosition(mySelectionMarker.getEndOffset());
}
}
else {
position = isVirtualSelectionEnabled() ? getVisualPosition() : myEditor.offsetToVisualPosition(getOffset());
}
if (hasVirtualSelection()) {
position = new VisualPosition(position.line, position.column + myEndVirtualOffset);
}
return position;
}
@Override
public boolean hasSelection() {
validateContext(false);
MyRangeMarker marker = mySelectionMarker;
return marker != null && marker.isValid() && (marker.getEndOffset() > marker.getStartOffset()
|| isVirtualSelectionEnabled() && myEndVirtualOffset > myStartVirtualOffset);
}
@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), startOffset, myEditor.offsetToVisualPosition(endOffset), 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);
}
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) : startPosition;
VisualPosition endPositionToUse = endPosition == null ? myEditor.offsetToVisualPosition(endOffset) : 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(new Runnable() {
public void run() {
int startOffset = _startOffset;
int endOffset = _endOffset;
myUnknownDirection = false;
final Document doc = myEditor.getDocument();
validateContext(true);
myEditor.getSelectionModel().removeBlockSelection();
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();
}
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
marker.release();
}
marker = new MyRangeMarker((DocumentEx)doc, startOffset, endOffset);
myStartVirtualOffset = 0;
myEndVirtualOffset = 0;
if (visualPositionAware) {
if (endPosition.after(startPosition)) {
marker.setStartPosition(startPosition);
marker.setEndPosition(endPosition);
marker.setEndPositionIsLead(false);
}
else {
marker.setStartPosition(endPosition);
marker.setEndPosition(startPosition);
marker.setEndPositionIsLead(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;
myStartVirtualOffset = Math.max(0, Math.min(startDiff, endDiff));
myEndVirtualOffset = 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(new Runnable() {
public void run() {
validateContext(true);
myEditor.getSelectionModel().removeBlockSelection();
int caretOffset = getOffset();
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
int startOffset = marker.getStartOffset();
int endOffset = marker.getEndOffset();
marker.release();
mySelectionMarker = null;
myStartVirtualOffset = 0;
myEndVirtualOffset = 0;
myEditor.getSelectionModel().fireSelectionChanged(startOffset, endOffset, caretOffset, caretOffset);
}
}
});
}
@Override
public int getLeadSelectionOffset() {
validateContext(false);
int caretOffset = getOffset();
if (hasSelection()) {
MyRangeMarker marker = mySelectionMarker;
if (marker != null) {
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() {
MyRangeMarker marker = mySelectionMarker;
VisualPosition caretPosition = getVisualPosition();
if (isVirtualSelectionEnabled() && !hasSelection()) {
return caretPosition;
}
if (marker == null) {
return caretPosition;
}
if (marker.isEndPositionIsLead()) {
VisualPosition result = marker.getEndPosition();
if (result == null) {
return getSelectionEndPosition();
}
else {
if (hasVirtualSelection()) {
result = new VisualPosition(result.line, result.column + myEndVirtualOffset);
}
return result;
}
}
else {
VisualPosition result = marker.getStartPosition();
if (result == null) {
return getSelectionStartPosition();
}
else {
if (hasVirtualSelection()) {
result = new VisualPosition(result.line, result.column + myStartVirtualOffset);
}
return result;
}
}
}
@Override
public void selectLineAtCaret() {
validateContext(true);
myEditor.getCaretModel().doWithCaretMerging(new Runnable() {
public void run() {
SelectionModelImpl.doSelectLineAtCaret(myEditor);
}
});
}
@Override
public void selectWordAtCaret(final boolean honorCamelWordsSettings) {
validateContext(true);
myEditor.getCaretModel().doWithCaretMerging(new Runnable() {
public void run() {
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, CaretImpl.this, myEditor.getDataContext());
}
finally {
if (needOverrideSetting) {
settings.resetCamelWords();
}
}
}
});
}
@Nullable
@Override
public String getSelectedText() {
if (!hasSelection()) {
return null;
}
CharSequence text = myEditor.getDocument().getCharsSequence();
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
String selectedText = text.subSequence(selectionStart, selectionEnd).toString();
if (isVirtualSelectionEnabled() && myEndVirtualOffset > myStartVirtualOffset) {
int padding = myEndVirtualOffset - myStartVirtualOffset;
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 void validateContext(boolean isWrite) {
if (!myEditor.getComponent().isShowing()) return;
if (isWrite) {
ApplicationManager.getApplication().assertIsDispatchThread();
}
else {
ApplicationManager.getApplication().assertReadAccessAllowed();
}
}
private boolean isVirtualSelectionEnabled() {
return myEditor.isColumnMode() && supportsMultipleCarets();
}
boolean hasVirtualSelection() {
validateContext(false);
MyRangeMarker marker = mySelectionMarker;
return marker != null && marker.isValid() && isVirtualSelectionEnabled() && myEndVirtualOffset > myStartVirtualOffset;
}
private int getCurrentX() {
return myEditor.visualPositionToXY(myVisibleCaret).x;
}
@Override
@NotNull
public EditorImpl getEditor() {
return myEditor;
}
@Override
public String toString() {
return "Caret at " + myVisibleCaret + (mySelectionMarker == null ? "" : (", selection marker: " + mySelectionMarker.toString()));
}
/**
* Encapsulates information about target vertical range info - its <code>'y'</code> coordinate and height in pixels.
*/
public static class VerticalInfo {
public final int y;
public final int height;
private VerticalInfo(int y, int height) {
this.y = y;
this.height = height;
}
}
private class MyRangeMarker extends RangeMarkerImpl {
private VisualPosition myStartPosition;
private VisualPosition myEndPosition;
private boolean myEndPositionIsLead;
private boolean myIsReleased;
MyRangeMarker(DocumentEx document, int start, int end) {
super(document, start, end, true);
myIsReleased = false;
}
public void release() {
myIsReleased = true;
dispose();
}
@Nullable
public VisualPosition getStartPosition() {
invalidateVisualPositions();
return myStartPosition;
}
public void setStartPosition(@NotNull VisualPosition startPosition) {
myStartPosition = startPosition;
}
@Nullable
public VisualPosition getEndPosition() {
invalidateVisualPositions();
return myEndPosition;
}
public void setEndPosition(@NotNull VisualPosition endPosition) {
myEndPosition = endPosition;
}
public boolean isEndPositionIsLead() {
return myEndPositionIsLead;
}
public void setEndPositionIsLead(boolean endPositionIsLead) {
myEndPositionIsLead = endPositionIsLead;
}
int startBefore;
int endBefore;
@Override
protected void changedUpdateImpl(DocumentEvent e) {
if (myIsReleased) return;
startBefore = getStartOffset();
endBefore = getEndOffset();
super.changedUpdateImpl(e);
}
private void invalidateVisualPositions() {
SoftWrapModelImpl model = myEditor.getSoftWrapModel();
if (!myEditor.offsetToVisualPosition(getStartOffset()).equals(myStartPosition) && model.getSoftWrap(getStartOffset()) == null
|| !myEditor.offsetToVisualPosition(getEndOffset()).equals(myEndPosition) && model.getSoftWrap(getEndOffset()) == null) {
myStartPosition = null;
myEndPosition = null;
}
}
}
}