private void paintBackground(
      Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine) {
    int lineCount = myEditor.getVisibleLineCount();
    final Map<Integer, Couple<Integer>> virtualSelectionMap =
        createVirtualSelectionMap(startVisualLine, endVisualLine);
    for (int visualLine = startVisualLine; visualLine <= endVisualLine; visualLine++) {
      int y = myView.visualLineToY(visualLine);
      LineLayout prefixLayout = myView.getPrefixLayout();
      if (visualLine == 0 && prefixLayout != null) {
        paintBackground(g, myView.getPrefixAttributes(), 0, y, prefixLayout.getWidth());
      }
      if (visualLine >= lineCount) break;
      paintLineFragments(
          g,
          clip,
          visualLine,
          y,
          new LineFragmentPainter() {
            @Override
            public void paintBeforeLineStart(
                Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
              paintBackground(g, attributes, 0, y, xEnd);
              paintSelectionOnSecondSoftWrapLineIfNecessary(g, columnEnd, xEnd, y);
            }

            @Override
            public void paint(
                Graphics2D g,
                VisualLineFragmentsIterator.Fragment fragment,
                int start,
                int end,
                TextAttributes attributes,
                float xStart,
                float xEnd,
                int y) {
              paintBackground(g, attributes, xStart, y, xEnd - xStart);
            }

            @Override
            public void paintAfterLineEnd(
                Graphics2D g, Rectangle clip, IterationState it, int columnStart, float x, int y) {
              paintBackground(
                  g, it.getPastLineEndBackgroundAttributes(), x, y, clip.x + clip.width - x);
              int offset = it.getEndOffset();
              SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset);
              if (softWrap == null) {
                paintVirtualSelectionIfNecessary(
                    g, virtualSelectionMap, columnStart, x, clip.x + clip.width, y);
              } else {
                paintSelectionOnFirstSoftWrapLineIfNecessary(
                    g, columnStart, x, clip.x + clip.width, y);
              }
            }
          });
    }
  }
  private void paintTextWithEffects(
      Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine) {
    final CharSequence text = myDocument.getImmutableCharSequence();
    final EditorImpl.LineWhitespacePaintingStrategy whitespacePaintingStrategy =
        myEditor.new LineWhitespacePaintingStrategy();
    int lineCount = myEditor.getVisibleLineCount();
    for (int visualLine = startVisualLine; visualLine <= endVisualLine; visualLine++) {
      int y = myView.visualLineToY(visualLine) + myView.getAscent();
      LineLayout prefixLayout = myView.getPrefixLayout();
      if (visualLine == 0 && prefixLayout != null) {
        g.setColor(myView.getPrefixAttributes().getForegroundColor());
        paintLineLayoutWithEffect(
            g,
            prefixLayout,
            0,
            y,
            myView.getPrefixAttributes().getEffectColor(),
            myView.getPrefixAttributes().getEffectType());
      }
      if (visualLine >= lineCount) break;

      final int[] currentLogicalLine = new int[] {-1};

      paintLineFragments(
          g,
          clip,
          visualLine,
          y,
          new LineFragmentPainter() {
            @Override
            public void paintBeforeLineStart(
                Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
              SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
              int symbolWidth =
                  softWrapModel.getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
              softWrapModel.paint(
                  g,
                  SoftWrapDrawingType.AFTER_SOFT_WRAP,
                  (int) xEnd - symbolWidth,
                  y - myView.getAscent(),
                  myView.getLineHeight());
            }

            @Override
            public void paint(
                Graphics2D g,
                VisualLineFragmentsIterator.Fragment fragment,
                int start,
                int end,
                TextAttributes attributes,
                float xStart,
                float xEnd,
                int y) {
              if (attributes != null && attributes.getForegroundColor() != null) {
                g.setColor(attributes.getForegroundColor());
                fragment.draw(g, xStart, y, start, end);
              }
              if (fragment.getCurrentFoldRegion() == null) {
                int logicalLine = fragment.getStartLogicalLine();
                if (logicalLine != currentLogicalLine[0]) {
                  whitespacePaintingStrategy.update(
                      text,
                      myDocument.getLineStartOffset(logicalLine),
                      myDocument.getLineEndOffset(logicalLine));
                  currentLogicalLine[0] = logicalLine;
                }
                paintWhitespace(
                    g, text, xStart, y, start, end, whitespacePaintingStrategy, fragment);
              }
              if (attributes != null
                  && hasTextEffect(attributes.getEffectColor(), attributes.getEffectType())) {
                paintTextEffect(
                    g, xStart, xEnd, y, attributes.getEffectColor(), attributes.getEffectType());
              }
            }

            @Override
            public void paintAfterLineEnd(
                Graphics2D g,
                Rectangle clip,
                IterationState iterationState,
                int columnStart,
                float x,
                int y) {
              int offset = iterationState.getEndOffset();
              SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
              if (softWrapModel.getSoftWrap(offset) == null) {
                int logicalLine = myDocument.getLineNumber(offset);
                paintLineExtensions(g, logicalLine, x, y);
              } else {
                softWrapModel.paint(
                    g,
                    SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED,
                    (int) x,
                    y - myView.getAscent(),
                    myView.getLineHeight());
              }
            }
          });
    }
  }
  public void moveCaretRelatively(
      int columnShift,
      int lineShift,
      boolean withSelection,
      boolean blockSelection,
      boolean scrollToCaret) {
    assertIsDispatchThread();
    SelectionModel selectionModel = myEditor.getSelectionModel();
    int selectionStart = selectionModel.getLeadSelectionOffset();
    LogicalPosition blockSelectionStart =
        selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : getLogicalPosition();
    EditorSettings editorSettings = myEditor.getSettings();
    VisualPosition visualCaret = getVisualPosition();

    int desiredX = myDesiredX;
    if (columnShift == 0) {
      if (myDesiredX < 0) {
        desiredX = myEditor.visualPositionToXY(visualCaret).x;
      }
    } else {
      myDesiredX = desiredX = -1;
    }

    int newLineNumber = visualCaret.line + lineShift;
    int newColumnNumber = visualCaret.column + columnShift;
    if (desiredX >= 0 && !ApplicationManager.getApplication().isUnitTestMode()) {
      newColumnNumber =
          myEditor.xyToVisualPosition(
                  new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight()))
              .column;
    }

    Document document = myEditor.getDocument();
    if (!editorSettings.isVirtualSpace()
        && columnShift == 0
        && getLogicalPosition().softWrapLinesOnCurrentLogicalLine <= 0) {
      newColumnNumber = myEditor.getLastColumnNumber();
    } else 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;
    }

    VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber);
    int lastColumnNumber = newColumnNumber;
    if (!editorSettings.isCaretInsideTabs() && !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.
        newColumnNumber = lastColumnNumber = Math.max(lastOffsetColumn, newColumnNumber);
        desiredX = -1;
      }
      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) {
        myEditor.setLastColumnNumber(lastColumnNumber);
      }
    }

    if (withSelection) {
      if (blockSelection) {
        selectionModel.setBlockSelection(blockSelectionStart, getLogicalPosition());
      } else {
        if (selectToDocumentStart) {
          selectionModel.setSelection(selectionStart, 0);
        } else if (pos.line >= myEditor.getVisibleLineCount()) {
          if (selectionStart < document.getTextLength()) {
            selectionModel.setSelection(selectionStart, document.getTextLength());
          }
        } else {
          selectionModel.setSelection(selectionStart, getVisualPosition(), getOffset());
        }
      }
    } else {
      selectionModel.removeSelection();
    }

    if (scrollToCaret) {
      myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
    }

    if (desiredX >= 0) {
      myDesiredX = desiredX;
    }

    EditorActionUtil.selectNonexpandableFold(myEditor);
  }
  public void moveToVisualPosition(@NotNull VisualPosition pos) {
    assertIsDispatchThread();
    validateCallContext();
    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));
    myOffset = myEditor.logicalPositionToOffset(myLogicalCaret);
    LOG.assertTrue(myOffset >= 0 && myOffset <= myEditor.getDocument().getTextLength());

    myVisualLineStart =
        myEditor.logicalPositionToOffset(
            myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line, 0)));
    myVisualLineEnd =
        myEditor.logicalPositionToOffset(
            myEditor.visualToLogicalPosition(new VisualPosition(myVisibleCaret.line + 1, 0)));

    ((FoldingModelImpl) myEditor.getFoldingModel()).flushCaretPosition();

    myEditor.setLastColumnNumber(myVisibleCaret.column);
    myEditor.updateCaretCursor();
    requestRepaint(oldInfo);

    if (oldPosition.column != myLogicalCaret.column || oldPosition.line != myLogicalCaret.line) {
      CaretEvent event = new CaretEvent(myEditor, oldPosition, myLogicalCaret);

      for (CaretListener listener : myCaretListeners) {
        listener.caretPositionChanged(event);
      }
    }
  }