[editor] refactor EditorPainter

GitOrigin-RevId: 057a1af994f3a1004188f53f40444c6132ba8162
This commit is contained in:
Alexander Trushev
2025-09-08 15:42:01 +00:00
committed by intellij-monorepo-bot
parent 035422b140
commit 4085f77da5
7 changed files with 363 additions and 266 deletions
@@ -0,0 +1,43 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.CaretModel;
import java.util.List;
/**
* Introduced within IDEA-94918 Option to show whitespaces only in selected (highlighted code)
*/
final class CaretDataInView {
private final int[] selectionStarts;
private final int[] selectionEnds;
private final int caretCount;
CaretDataInView(CaretModel caretModel, int startOffset, int endOffset) {
List<Caret> carets = caretModel.getAllCarets();
selectionStarts = new int[carets.size()];
selectionEnds = new int[carets.size()];
int i = 0;
for (Caret caret : carets) {
if (!caret.hasSelection()) continue;
if (caret.getSelectionStart() >= endOffset || caret.getSelectionEnd() <= startOffset) {
continue;
}
selectionStarts[i] = caret.getSelectionStart();
selectionEnds[i] = caret.getSelectionEnd();
++i;
}
caretCount = i;
}
boolean isOffsetInSelection(int offset) {
for (int i = 0; i < caretCount; ++i) {
if (offset >= selectionStarts[i] && offset < selectionEnds[i]) {
return true;
}
}
return false;
}
}
@@ -34,7 +34,6 @@ import com.intellij.ui.scale.ScaleContext;
import com.intellij.util.*;
import com.intellij.util.containers.PeekableIterator;
import com.intellij.util.containers.PeekableIteratorWrapper;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.UIUtil;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
import it.unimi.dsi.fastutil.floats.FloatList;
@@ -58,8 +57,6 @@ public final class EditorPainter implements TextDrawingCallback {
private static final Color CARET_LIGHT = Gray._255;
private static final Color CARET_DARK = Gray._0;
private static final int CARET_DIRECTION_MARK_SIZE = 3;
private static final char IDEOGRAPHIC_SPACE = '\u3000';
private static final String WHITESPACE_CHARS = " \t" + IDEOGRAPHIC_SPACE;
private static final Object ourCachedDot = ObjectUtils.sentinel("space symbol");
@ApiStatus.Internal
@@ -79,6 +76,11 @@ public final class EditorPainter implements TextDrawingCallback {
(Registry.is("editor.show.right.margin.in.read.only.files") || editor.getDocument().isWritable());
}
static float getBaseMarginWidth(@NotNull EditorView view) {
Editor editor = view.getEditor();
return editor.getSettings().getRightMargin(editor.getProject()) * view.getPlainSpaceWidth();
}
private final EditorView myView;
EditorPainter(EditorView view) {
@@ -279,11 +281,11 @@ public final class EditorPainter implements TextDrawingCallback {
LinePainter2D.paint(myGraphics, baseMarginX, 0, baseMarginX, myClip.height);
}
else {
int displayedLinesCount = myMarginPositions.x.length - 1;
int displayedLinesCount = myMarginPositions.x().length - 1;
for (int i = 0; i <= displayedLinesCount; i++) {
float width = myMarginPositions.x[i];
float width = myMarginPositions.x()[i];
int x = width == 0 ? baseMarginX : (int)width;
int y = myMarginPositions.y[i];
int y = myMarginPositions.y()[i];
if (i == 0 && y > myYShift) {
myGraphics.fillRect(baseMarginX, myYShift, 1, y - myYShift);
if (x != baseMarginX) {
@@ -292,9 +294,9 @@ public final class EditorPainter implements TextDrawingCallback {
}
if (i < displayedLinesCount) {
myGraphics.fillRect(x, y, 1, myLineHeight);
float nextWidth = myMarginPositions.x[i + 1];
float nextWidth = myMarginPositions.x()[i + 1];
int nextX = nextWidth == 0 ? baseMarginX : (int)nextWidth;
int nextY = myMarginPositions.y[i + 1];
int nextY = myMarginPositions.y()[i + 1];
if (nextY > y + myLineHeight) {
if (x != baseMarginX) {
myGraphics.fillRect(Math.min(x, baseMarginX), y + myLineHeight - 1, Math.abs(x - baseMarginX) + 1, 1);
@@ -317,11 +319,6 @@ public final class EditorPainter implements TextDrawingCallback {
}
}
private static float getBaseMarginWidth(EditorView view) {
Editor editor = view.getEditor();
return editor.getSettings().getRightMargin(editor.getProject()) * view.getPlainSpaceWidth();
}
private boolean isMarginShown() {
return EditorPainter.isMarginShown(myEditor);
}
@@ -369,7 +366,7 @@ public final class EditorPainter implements TextDrawingCallback {
int visualLine = visLinesIterator.getVisualLine();
if (visualLine > myEndVisualLine + 1) break;
int y = visLinesIterator.getY() + myYShift;
if (calculateMarginWidths) myMarginPositions.y[visualLine - myStartVisualLine] = y;
if (calculateMarginWidths) myMarginPositions.y()[visualLine - myStartVisualLine] = y;
if (y > prevY) {
TextAttributes attributes = getBetweenLinesAttributes(visualLine, visLinesIterator.getVisualLineStartOffset(),
Objects.requireNonNull(caretIterator));
@@ -498,12 +495,12 @@ public final class EditorPainter implements TextDrawingCallback {
}
}
}, calculateMarginWidths && !visLinesIterator.endsWithSoftWrap() && !visLinesIterator.startsWithSoftWrap()
? width -> myMarginPositions.x[visualLine - myStartVisualLine] = width : null);
? width -> myMarginPositions.x()[visualLine - myStartVisualLine] = width : null);
prevY = y + visLinesIterator.getLineHeight();
visLinesIterator.advance();
}
if (calculateMarginWidths && myEndVisualLine >= lineCount - 1) {
myMarginPositions.y[myMarginPositions.y.length - 1] = myMarginPositions.y[myMarginPositions.y.length - 2] + myLineHeight;
myMarginPositions.y()[myMarginPositions.y().length - 1] = myMarginPositions.y()[myMarginPositions.y().length - 2] + myLineHeight;
}
}
@@ -904,8 +901,8 @@ public final class EditorPainter implements TextDrawingCallback {
List<LineExtensionData> data = myExtensionData.get(visualLine);
if (data == null) return;
for (LineExtensionData datum : data) {
float width = datum.layout.getWidth();
paintBackground(datum.info.getBgColor(), x, y, width);
float width = datum.layout().getWidth();
paintBackground(datum.info().getBgColor(), x, y, width);
x += width;
}
}
@@ -914,7 +911,7 @@ public final class EditorPainter implements TextDrawingCallback {
List<LineExtensionData> data = myExtensionData.get(visualLine);
if (data == null) return;
for (LineExtensionData datum : data) {
x = paintLineLayoutWithEffect(datum.layout, x, y, datum.info.getColor(), datum.info.getEffectColor(), datum.info.getEffectType());
x = paintLineLayoutWithEffect(datum.layout(), x, y, datum.info().getColor(), datum.info().getEffectColor(), datum.info().getEffectType());
}
int currentLineWidth = myCorrector.lineWidth(visualLine, x);
EditorSizeManager sizeManager = myView.getSizeManager();
@@ -1656,251 +1653,4 @@ public final class EditorPainter implements TextDrawingCallback {
return foldAttributes;
}
}
interface LineFragmentPainter {
void paintBeforeLineStart(TextAttributes attributes, boolean hasSoftWrap, int columnEnd, float xEnd, int y);
void paint(VisualLineFragmentsIterator.Fragment fragment, int start, int end, TextAttributes attributes,
float xStart, float xEnd, int y);
void paintAfterLineEnd(IterationState iterationState, int columnStart, float x, int y);
}
private static final class LineWhitespacePaintingStrategy {
private final boolean myWhitespaceShown;
private final boolean myLeadingWhitespaceShown;
private final boolean myInnerWhitespaceShown;
private final boolean myTrailingWhitespaceShown;
private final boolean mySelectionWhitespaceShown;
// Offsets on current line where leading whitespace ends and trailing whitespace starts correspondingly.
private int currentLeadingEdge;
private int currentTrailingEdge;
LineWhitespacePaintingStrategy(EditorSettings settings) {
myWhitespaceShown = settings.isWhitespacesShown();
myLeadingWhitespaceShown = settings.isLeadingWhitespaceShown();
myInnerWhitespaceShown = settings.isInnerWhitespaceShown();
myTrailingWhitespaceShown = settings.isTrailingWhitespaceShown();
mySelectionWhitespaceShown = settings.isSelectionWhitespaceShown();
}
private boolean showAnyWhitespace() {
return myWhitespaceShown && (myLeadingWhitespaceShown || myInnerWhitespaceShown || myTrailingWhitespaceShown || mySelectionWhitespaceShown);
}
private void update(CharSequence chars, int lineStart, int lineEnd) {
if (showAnyWhitespace() && !(myLeadingWhitespaceShown && myInnerWhitespaceShown && myTrailingWhitespaceShown)) {
currentTrailingEdge = CharArrayUtil.shiftBackward(chars, lineStart, lineEnd - 1, WHITESPACE_CHARS) + 1;
currentLeadingEdge = CharArrayUtil.shiftForward(chars, lineStart, currentTrailingEdge, WHITESPACE_CHARS);
}
}
private boolean showWhitespaceAtOffset(int offset, CaretDataInView caretData) {
if (!myWhitespaceShown) return false;
if (offset < currentLeadingEdge ? myLeadingWhitespaceShown :
offset >= currentTrailingEdge ? myTrailingWhitespaceShown :
myInnerWhitespaceShown) {
return true;
} else {
return mySelectionWhitespaceShown && caretData != null && caretData.isOffsetInSelection(offset);
}
}
}
private interface XCorrector {
float startX(int line);
int lineWidth(int line, float x);
int emptyTextX();
int minX(int startLine, int endLine);
int maxX(int startLine, int endLine);
int lineSeparatorStart(int minX);
int lineSeparatorEnd(int maxX);
float singleLineBorderStart(float x);
float singleLineBorderEnd(float x);
int marginX(float marginWidth);
List<Integer> softMarginsX();
static @NotNull XCorrector create(@NotNull EditorView view, @NotNull Insets insets) {
return view.getEditor().isRightAligned() ? new RightAligned(view) : new LeftAligned(view, insets);
}
final class LeftAligned implements XCorrector {
private final EditorView myView;
private final int myLeftInset;
private LeftAligned(@NotNull EditorView view, @NotNull Insets insets) {
myView = view;
myLeftInset = insets.left;
}
@Override
public float startX(int line) {
return myLeftInset;
}
@Override
public int emptyTextX() {
return myLeftInset;
}
@Override
public int minX(int startLine, int endLine) {
return myLeftInset;
}
@Override
public int maxX(int startLine, int endLine) {
return minX(startLine, endLine) + myView.getMaxTextWidthInLineRange(startLine, endLine - 1) - 1;
}
@Override
public float singleLineBorderStart(float x) {
return x;
}
@Override
public float singleLineBorderEnd(float x) {
return x + 1;
}
@Override
public int lineWidth(int line, float x) {
return (int)x - myLeftInset;
}
@Override
public int lineSeparatorStart(int maxX) {
return myLeftInset;
}
@Override
public int lineSeparatorEnd(int maxX) {
return isMarginShown(myView.getEditor()) ? Math.min(marginX(Session.getBaseMarginWidth(myView)), maxX) : maxX;
}
@Override
public int marginX(float marginWidth) {
return (int)(myLeftInset + marginWidth);
}
@Override
public List<Integer> softMarginsX() {
List<Integer> margins = myView.getEditor().getSettings().getSoftMargins();
List<Integer> result = new ArrayList<>(margins.size());
for (Integer margin : margins) {
result.add((int)(myLeftInset + margin * myView.getPlainSpaceWidth()));
}
return result;
}
}
final class RightAligned implements XCorrector {
private final EditorView myView;
private RightAligned(@NotNull EditorView view) {
myView = view;
}
@Override
public float startX(int line) {
return myView.getRightAlignmentLineStartX(line);
}
@Override
public int lineWidth(int line, float x) {
return (int)(x - myView.getRightAlignmentLineStartX(line));
}
@Override
public int emptyTextX() {
return myView.getRightAlignmentMarginX();
}
@Override
public int minX(int startLine, int endLine) {
return myView.getRightAlignmentMarginX() - myView.getMaxTextWidthInLineRange(startLine, endLine - 1) - 1;
}
@Override
public int maxX(int startLine, int endLine) {
return myView.getRightAlignmentMarginX() - 1;
}
@Override
public float singleLineBorderStart(float x) {
return x - 1;
}
@Override
public float singleLineBorderEnd(float x) {
return x;
}
@Override
public int lineSeparatorStart(int minX) {
return isMarginShown(myView.getEditor()) ? Math.max(marginX(Session.getBaseMarginWidth(myView)), minX) : minX;
}
@Override
public int lineSeparatorEnd(int maxX) {
return maxX;
}
@Override
public int marginX(float marginWidth) {
return (int)(myView.getRightAlignmentMarginX() - marginWidth);
}
@Override
public List<Integer> softMarginsX() {
List<Integer> margins = myView.getEditor().getSettings().getSoftMargins();
List<Integer> result = new ArrayList<>(margins.size());
for (Integer margin : margins) {
result.add((int)(myView.getRightAlignmentMarginX() - margin * myView.getPlainSpaceWidth()));
}
return result;
}
}
}
private record LineExtensionData(LineExtensionInfo info, LineLayout layout) {
}
private static final class MarginPositions {
private final float[] x;
private final int[] y;
private MarginPositions(int size) {
x = new float[size];
y = new int[size];
}
}
private static final class CaretDataInView {
private final int[] selectionStarts;
private final int[] selectionEnds;
private final int caretCount;
private CaretDataInView(CaretModel caretModel, int startOffset, int endOffset) {
List<Caret> carets = caretModel.getAllCarets();
selectionStarts = new int[carets.size()];
selectionEnds = new int[carets.size()];
int i = 0;
for (Caret caret : carets) {
if (!caret.hasSelection()) continue;
if (caret.getSelectionStart() >= endOffset || caret.getSelectionEnd() <= startOffset) continue;
selectionStarts[i] = caret.getSelectionStart();
selectionEnds[i] = caret.getSelectionEnd();
++i;
}
caretCount = i;
}
private boolean isOffsetInSelection(int offset) {
for (int i = 0; i < caretCount; ++i) {
if (offset >= selectionStarts[i] && offset < selectionEnds[i]) return true;
}
return false;
}
}
}
@@ -0,0 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
import com.intellij.openapi.editor.LineExtensionInfo;
/**
* Introduced within IDEA-136087 Debugger: Inlined values: background settings are ignored
*/
record LineExtensionData(LineExtensionInfo info, LineLayout layout) {
}
@@ -0,0 +1,33 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
import com.intellij.openapi.editor.markup.TextAttributes;
interface LineFragmentPainter {
void paintBeforeLineStart(
TextAttributes attributes,
boolean hasSoftWrap,
int columnEnd,
float xEnd,
int y
);
void paint(
VisualLineFragmentsIterator.Fragment fragment,
int start,
int end,
TextAttributes attributes,
float xStart,
float xEnd,
int y
);
void paintAfterLineEnd(
IterationState iterationState,
int columnStart,
float x,
int y
);
}
@@ -0,0 +1,62 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
final class LineWhitespacePaintingStrategy {
private static final char IDEOGRAPHIC_SPACE = '\u3000';
private static final String WHITESPACE_CHARS = " \t" + IDEOGRAPHIC_SPACE;
private final boolean whitespaceShown;
private final boolean leadingWhitespaceShown;
private final boolean innerWhitespaceShown;
private final boolean trailingWhitespaceShown;
private final boolean selectionWhitespaceShown;
// Offsets on current line where leading whitespace ends and trailing whitespace starts correspondingly.
private int currentLeadingEdge;
private int currentTrailingEdge;
LineWhitespacePaintingStrategy(@NotNull EditorSettings settings) {
whitespaceShown = settings.isWhitespacesShown();
leadingWhitespaceShown = settings.isLeadingWhitespaceShown();
innerWhitespaceShown = settings.isInnerWhitespaceShown();
trailingWhitespaceShown = settings.isTrailingWhitespaceShown();
selectionWhitespaceShown = settings.isSelectionWhitespaceShown();
}
boolean showAnyWhitespace() {
if (whitespaceShown) {
if (leadingWhitespaceShown || innerWhitespaceShown || trailingWhitespaceShown || selectionWhitespaceShown) {
return true;
}
}
return false;
}
void update(CharSequence chars, int lineStart, int lineEnd) {
if (showAnyWhitespace() && !(leadingWhitespaceShown && innerWhitespaceShown && trailingWhitespaceShown)) {
currentTrailingEdge = CharArrayUtil.shiftBackward(chars, lineStart, lineEnd - 1, WHITESPACE_CHARS) + 1;
currentLeadingEdge = CharArrayUtil.shiftForward(chars, lineStart, currentTrailingEdge, WHITESPACE_CHARS);
}
}
boolean showWhitespaceAtOffset(int offset, CaretDataInView caretData) {
if (!whitespaceShown) {
return false;
}
if (offset < currentLeadingEdge
? leadingWhitespaceShown
: offset >= currentTrailingEdge
? trailingWhitespaceShown
: innerWhitespaceShown) {
return true;
} else {
return selectionWhitespaceShown && caretData != null && caretData.isOffsetInSelection(offset);
}
}
}
@@ -0,0 +1,24 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
/**
* Introduced within IDEA-183815 Capability to add a content between code lines in the editor
*/
final class MarginPositions {
private final float[] x;
private final int[] y;
MarginPositions(int size) {
x = new float[size];
y = new int[size];
}
float[] x() {
return x;
}
int[] y() {
return y;
}
}
@@ -0,0 +1,174 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl.view;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* IDEA: Editor horizontal text alignment
*/
interface XCorrector {
float startX(int line);
int lineWidth(int line, float x);
int emptyTextX();
int minX(int startLine, int endLine);
int maxX(int startLine, int endLine);
int lineSeparatorStart(int minX);
int lineSeparatorEnd(int maxX);
float singleLineBorderStart(float x);
float singleLineBorderEnd(float x);
int marginX(float marginWidth);
List<Integer> softMarginsX();
static @NotNull XCorrector create(@NotNull EditorView view, @NotNull Insets insets) {
return view.getEditor().isRightAligned() ? new RightAligned(view) : new LeftAligned(view, insets);
}
final class LeftAligned implements XCorrector {
private final EditorView myView;
private final int myLeftInset;
private LeftAligned(@NotNull EditorView view, @NotNull Insets insets) {
myView = view;
myLeftInset = insets.left;
}
@Override
public float startX(int line) {
return myLeftInset;
}
@Override
public int emptyTextX() {
return myLeftInset;
}
@Override
public int minX(int startLine, int endLine) {
return myLeftInset;
}
@Override
public int maxX(int startLine, int endLine) {
return minX(startLine, endLine) + myView.getMaxTextWidthInLineRange(startLine, endLine - 1) - 1;
}
@Override
public float singleLineBorderStart(float x) {
return x;
}
@Override
public float singleLineBorderEnd(float x) {
return x + 1;
}
@Override
public int lineWidth(int line, float x) {
return (int)x - myLeftInset;
}
@Override
public int lineSeparatorStart(int maxX) {
return myLeftInset;
}
@Override
public int lineSeparatorEnd(int maxX) {
return EditorPainter.isMarginShown(myView.getEditor()) ? Math.min(marginX(EditorPainter.getBaseMarginWidth(myView)), maxX) : maxX;
}
@Override
public int marginX(float marginWidth) {
return (int)(myLeftInset + marginWidth);
}
/**
* Visual indent guides (soft margins) initial implementation (IDEA-99875)
*/
@Override
public List<Integer> softMarginsX() {
List<Integer> margins = myView.getEditor().getSettings().getSoftMargins();
List<Integer> result = new ArrayList<>(margins.size());
for (Integer margin : margins) {
result.add((int)(myLeftInset + margin * myView.getPlainSpaceWidth()));
}
return result;
}
}
final class RightAligned implements XCorrector {
private final EditorView myView;
private RightAligned(@NotNull EditorView view) {
myView = view;
}
@Override
public float startX(int line) {
return myView.getRightAlignmentLineStartX(line);
}
@Override
public int lineWidth(int line, float x) {
return (int)(x - myView.getRightAlignmentLineStartX(line));
}
@Override
public int emptyTextX() {
return myView.getRightAlignmentMarginX();
}
@Override
public int minX(int startLine, int endLine) {
return myView.getRightAlignmentMarginX() - myView.getMaxTextWidthInLineRange(startLine, endLine - 1) - 1;
}
@Override
public int maxX(int startLine, int endLine) {
return myView.getRightAlignmentMarginX() - 1;
}
@Override
public float singleLineBorderStart(float x) {
return x - 1;
}
@Override
public float singleLineBorderEnd(float x) {
return x;
}
@Override
public int lineSeparatorStart(int minX) {
return EditorPainter.isMarginShown(myView.getEditor()) ? Math.max(marginX(EditorPainter.getBaseMarginWidth(myView)), minX) : minX;
}
@Override
public int lineSeparatorEnd(int maxX) {
return maxX;
}
@Override
public int marginX(float marginWidth) {
return (int)(myView.getRightAlignmentMarginX() - marginWidth);
}
/**
* Visual indent guides (soft margins) initial implementation (IDEA-99875)
*/
@Override
public List<Integer> softMarginsX() {
List<Integer> margins = myView.getEditor().getSettings().getSoftMargins();
List<Integer> result = new ArrayList<>(margins.size());
for (Integer margin : margins) {
result.add((int)(myView.getRightAlignmentMarginX() - margin * myView.getPlainSpaceWidth()));
}
return result;
}
}
}