| /* |
| * 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; |
| } |
| } |
| } |
| } |