/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.editor.impl.view; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.impl.FoldingModelImpl; import com.intellij.openapi.editor.impl.SoftWrapModelImpl; import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType; import com.intellij.util.DocumentUtil; import org.jetbrains.annotations.NotNull; import java.awt.geom.Point2D; import java.util.List; /** * Performs transformations between various location representations in editor * (offset, logical position, visual position, pixel coordinates). * * @see LogicalPosition * @see VisualPosition */ class EditorCoordinateMapper { private static final Logger LOG = Logger.getInstance(EditorCoordinateMapper.class); private final EditorView myView; private final Document myDocument; private final FoldingModelImpl myFoldingModel; EditorCoordinateMapper(EditorView view) { myView = view; myDocument = myView.getEditor().getDocument(); myFoldingModel = myView.getEditor().getFoldingModel(); } int visualLineToY(int line) { return myView.getInsets().top + Math.max(0, line) * myView.getLineHeight(); } int yToVisualLine(int y) { return Math.max(0, y - myView.getInsets().top) / myView.getLineHeight(); } @NotNull LogicalPosition offsetToLogicalPosition(int offset) { return myView.getLogicalPositionCache().offsetToLogicalPosition(offset); } int logicalPositionToOffset(@NotNull LogicalPosition pos) { return myView.getLogicalPositionCache().logicalPositionToOffset(pos); } @NotNull VisualPosition logicalToVisualPosition(@NotNull LogicalPosition pos, boolean beforeSoftWrap) { int line = pos.line; int column = pos.column; int logicalLineCount = myDocument.getLineCount(); if (line >= logicalLineCount) { return new VisualPosition(line - logicalLineCount + myView.getEditor().getVisibleLineCount(), column, pos.leansForward); } int offset = logicalPositionToOffset(pos); int visualLine = offsetToVisualLine(offset, beforeSoftWrap); int maxVisualColumn = 0; int maxLogicalColumn = 0; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap)) { if (!pos.leansForward && offset == fragment.getVisualLineStartOffset()) { return new VisualPosition(visualLine, fragment.getStartVisualColumn()); } if (fragment.isCollapsedFoldRegion()) { int startLogicalLine = fragment.getStartLogicalLine(); int endLogicalLine = fragment.getEndLogicalLine(); int startLogicalColumn = fragment.getStartLogicalColumn(); int endLogicalColumn = fragment.getEndLogicalColumn(); if ((line > startLogicalLine || line == startLogicalLine && (column > startLogicalColumn || column == startLogicalColumn && pos.leansForward)) && (line < endLogicalLine || line == endLogicalLine && column < endLogicalColumn)) { return new VisualPosition(visualLine, fragment.getStartVisualColumn(), true); } if (line == endLogicalLine && column == endLogicalColumn && !pos.leansForward) { return new VisualPosition(visualLine, fragment.getEndVisualColumn()); } maxLogicalColumn = startLogicalLine == endLogicalLine ? Math.max(maxLogicalColumn, endLogicalColumn) : endLogicalColumn; } else if (fragment.getCurrentInlays() == null) { int minColumn = fragment.getMinLogicalColumn(); int maxColumn = fragment.getMaxLogicalColumn(); if (line == fragment.getStartLogicalLine() && (column > minColumn && column < maxColumn || column == minColumn && pos.leansForward || column == maxColumn && !pos.leansForward)) { return new VisualPosition(visualLine, fragment.logicalToVisualColumn(column), fragment.isRtl() ^ pos.leansForward); } maxLogicalColumn = Math.max(maxLogicalColumn, maxColumn); } maxVisualColumn = fragment.getEndVisualColumn(); } int resultColumn = column - maxLogicalColumn + maxVisualColumn; if (resultColumn < 0) { if (maxVisualColumn > maxLogicalColumn) { resultColumn = Integer.MAX_VALUE; // guarding against overflow } else { LOG.error("Error converting " + pos + " to visual position", new Attachment("details.txt", String.format("offset: %d, visual line: %d, max logical column: %d, max visual column: %d", offset, visualLine, maxLogicalColumn, maxVisualColumn)), new Attachment("dump.txt", myView.getEditor().dumpState())); resultColumn = 0; } } return new VisualPosition(visualLine, resultColumn, pos.leansForward); } @NotNull LogicalPosition visualToLogicalPosition(@NotNull VisualPosition pos) { int line = pos.line; int column = pos.column; int visualLineCount = myView.getEditor().getVisibleLineCount(); if (line >= visualLineCount) { return new LogicalPosition(line - visualLineCount + myDocument.getLineCount(), column, pos.leansRight); } int offset = visualLineToOffset(line); int logicalLine = myDocument.getLineNumber(offset); int maxVisualColumn = 0; int maxLogicalColumn = 0; int maxOffset = offset; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false)) { int minColumn = fragment.getStartVisualColumn(); int maxColumn = fragment.getEndVisualColumn(); if (column < minColumn || column == minColumn && !pos.leansRight) { return offsetToLogicalPosition(offset); } if (column > minColumn && column < maxColumn || column == minColumn || column == maxColumn && !pos.leansRight) { return new LogicalPosition(column == maxColumn ? fragment.getEndLogicalLine() : fragment.getStartLogicalLine(), fragment.visualToLogicalColumn(column), fragment.isCollapsedFoldRegion() ? column < maxColumn : fragment.getCurrentInlays() != null ? column == maxColumn : fragment.isRtl() ^ pos.leansRight); } maxLogicalColumn = logicalLine == fragment.getEndLogicalLine() ? Math.max(maxLogicalColumn, fragment.getMaxLogicalColumn()) : fragment.getMaxLogicalColumn(); maxVisualColumn = maxColumn; logicalLine = fragment.getEndLogicalLine(); maxOffset = Math.max(maxOffset, fragment.getMaxOffset()); } if (myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) == null) { int resultColumn = column - maxVisualColumn + maxLogicalColumn; if (resultColumn < 0 && maxLogicalColumn > maxVisualColumn) { resultColumn = Integer.MAX_VALUE; // guarding against overflow } return new LogicalPosition(logicalLine, resultColumn, true); } else { return offsetToLogicalPosition(maxOffset).leanForward(true); } } @NotNull VisualPosition offsetToVisualPosition(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) { return logicalToVisualPosition(offsetToLogicalPosition(offset).leanForward(leanTowardsLargerOffsets), beforeSoftWrap); } int visualPositionToOffset(VisualPosition visualPosition) { return logicalPositionToOffset(visualToLogicalPosition(visualPosition)); } int offsetToVisualLine(int offset, boolean beforeSoftWrap) { int textLength = myDocument.getTextLength(); if (offset < 0 || textLength == 0) { return 0; } offset = Math.min(offset, textLength); offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset); FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { assert outermostCollapsed.isValid(); offset = outermostCollapsed.getStartOffset(); beforeSoftWrap = false; } int wrapIndex = myView.getEditor().getSoftWrapModel().getSoftWrapIndex(offset); int softWrapsBeforeOrAtOffset = wrapIndex < 0 ? (- wrapIndex - 1) : wrapIndex + (beforeSoftWrap ? 0 : 1); return myDocument.getLineNumber(offset) - myFoldingModel.getFoldedLinesCountBefore(offset) + softWrapsBeforeOrAtOffset; } int visualLineToOffset(int visualLine) { int start = 0; int end = myDocument.getTextLength(); if (visualLine <= 0) return start; if (visualLine >= myView.getEditor().getVisibleLineCount()) return end; int current = 0; while (start <= end) { current = (start + end) / 2; int line = offsetToVisualLine(current, false); if (line < visualLine) { start = current + 1; } else if (line > visualLine) { end = current - 1; } else { break; } } return visualLineStartOffset(current, true); } private int visualLineStartOffset(int offset, boolean leanForward) { EditorImpl editor = myView.getEditor(); offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset); int result = EditorUtil.getNotFoldedLineStartOffset(editor, offset); SoftWrapModelImpl softWrapModel = editor.getSoftWrapModel(); List<? extends SoftWrap> softWraps = softWrapModel.getRegisteredSoftWraps(); int currentOrPrevWrapIndex = softWrapModel.getSoftWrapIndex(offset); SoftWrap currentOrPrevWrap; if (currentOrPrevWrapIndex < 0) { currentOrPrevWrapIndex = - currentOrPrevWrapIndex - 2; currentOrPrevWrap = currentOrPrevWrapIndex < 0 || currentOrPrevWrapIndex >= softWraps.size() ? null : softWraps.get(currentOrPrevWrapIndex); } else { currentOrPrevWrap = leanForward ? softWraps.get(currentOrPrevWrapIndex) : null; } if (currentOrPrevWrap != null && currentOrPrevWrap.getStart() > result) { result = currentOrPrevWrap.getStart(); } return result; } private float getStartX(int line) { return myView.getInsets().left + (line == 0 ? myView.getPrefixTextWidthInPixels() : 0); } @NotNull VisualPosition xyToVisualPosition(@NotNull Point2D p) { int visualLine = yToVisualLine((int)p.getY()); int lastColumn = 0; float x = getStartX(visualLine); float px = (float)p.getX(); if (visualLine < myView.getEditor().getVisibleLineCount()) { int visualLineStartOffset = visualLineToOffset(visualLine); int maxOffset = 0; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false)) { if (px <= fragment.getStartX()) { if (fragment.getStartVisualColumn() == 0) { return new VisualPosition(visualLine, 0); } int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP); float indent = fragment.getStartX() - markerWidth; if (px <= indent) { break; } boolean after = px >= indent + markerWidth / 2; return new VisualPosition(visualLine, fragment.getStartVisualColumn() - (after ? 0 : 1), !after); } float nextX = fragment.getEndX(); if (px <= nextX) { int[] column = fragment.xToVisualColumn(px); return new VisualPosition(visualLine, column[0], column[1] > 0); } x = nextX; lastColumn = fragment.getEndVisualColumn(); maxOffset = Math.max(maxOffset, fragment.getMaxOffset()); } if (myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) != null) { int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); if (px <= x + markerWidth) { boolean after = px >= x + markerWidth / 2; return new VisualPosition(visualLine, lastColumn + (after ? 1 : 0), !after); } px -= markerWidth; lastColumn++; } } int plainSpaceWidth = myView.getPlainSpaceWidth(); int remainingShift = (int)(px - x); int additionalColumns = remainingShift <= 0 ? 0 : (remainingShift + plainSpaceWidth / 2) / plainSpaceWidth; return new VisualPosition(visualLine, lastColumn + additionalColumns, remainingShift > 0 && additionalColumns == (remainingShift - 1) / plainSpaceWidth); } @NotNull Point2D visualPositionToXY(@NotNull VisualPosition pos) { int visualLine = pos.line; int column = pos.column; int y = visualLineToY(visualLine); float x = getStartX(visualLine); int lastColumn = 0; if (visualLine < myView.getEditor().getVisibleLineCount()) { int visualLineStartOffset = visualLineToOffset(visualLine); int maxOffset = 0; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false)) { int startVisualColumn = fragment.getStartVisualColumn(); if (column < startVisualColumn || column == startVisualColumn && !pos.leansRight) { break; } int endColumn = fragment.getEndVisualColumn(); if (column < endColumn || column == endColumn && !pos.leansRight) { return new Point2D.Float(fragment.visualColumnToX(column), y); } x = fragment.getEndX(); lastColumn = endColumn; maxOffset = Math.max(maxOffset, fragment.getMaxOffset()); } if (column > lastColumn && myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) != null) { column--; x += myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); } } int additionalShift = column <= lastColumn ? 0 : (column - lastColumn) * myView.getPlainSpaceWidth(); return new Point2D.Float(x + additionalShift, y); } @NotNull Point2D offsetToXY(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) { offset = Math.max(0, Math.min(myDocument.getTextLength(), offset)); offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset); int logicalLine = myDocument.getLineNumber(offset); int visualLine = offsetToVisualLine(offset, beforeSoftWrap); int visualLineStartOffset = visualLineToOffset(visualLine); int y = visualLineToY(visualLine); float x = getStartX(logicalLine); if (myDocument.getTextLength() > 0) { boolean firstFragment = true; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap)) { if (firstFragment && offset == visualLineStartOffset && !leanTowardsLargerOffsets) { x = fragment.getStartX(); break; } firstFragment = false; int minOffset = fragment.getMinOffset(); int maxOffset = fragment.getMaxOffset(); if (fragment.getCurrentInlays() == null && (offset > minOffset && offset < maxOffset || offset == minOffset && leanTowardsLargerOffsets || offset == maxOffset && !leanTowardsLargerOffsets)) { x = fragment.offsetToX(offset); break; } else { x = fragment.getEndX(); } } } return new Point2D.Float(x, y); } }