replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / view / EditorPainter.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.editor.impl.view;
17
18 import com.intellij.openapi.editor.*;
19 import com.intellij.openapi.editor.colors.EditorColors;
20 import com.intellij.openapi.editor.colors.EditorFontType;
21 import com.intellij.openapi.editor.ex.MarkupModelEx;
22 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
23 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
24 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
25 import com.intellij.openapi.editor.impl.*;
26 import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType;
27 import com.intellij.openapi.editor.markup.*;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.util.Couple;
30 import com.intellij.openapi.util.TextRange;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.openapi.wm.impl.IdeBackgroundUtil;
33 import com.intellij.ui.ColorUtil;
34 import com.intellij.ui.Gray;
35 import com.intellij.ui.JBColor;
36 import com.intellij.ui.paint.EffectPainter;
37 import com.intellij.util.DocumentUtil;
38 import com.intellij.util.text.CharArrayUtil;
39 import com.intellij.util.ui.JBUI;
40 import com.intellij.util.ui.UIUtil;
41 import gnu.trove.TFloatArrayList;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44
45 import javax.swing.*;
46 import java.awt.*;
47 import java.awt.geom.*;
48 import java.util.Collection;
49 import java.util.HashMap;
50 import java.util.Map;
51
52 /**
53  * Renders editor contents.
54  */
55 class EditorPainter implements TextDrawingCallback {
56   private static final Color CARET_LIGHT = Gray._255;
57   private static final Color CARET_DARK = Gray._0;
58   private static final Stroke IME_COMPOSED_TEXT_UNDERLINE_STROKE = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0,
59                                                                                    new float[]{0, 2, 0, 2}, 0);
60   private static final int CARET_DIRECTION_MARK_SIZE = 5;
61   private static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space
62   private static final String WHITESPACE_CHARS = " \t" + IDEOGRAPHIC_SPACE;
63
64
65   private final EditorView myView;
66   private final EditorImpl myEditor;
67   private final Document myDocument;
68
69   EditorPainter(EditorView view) {
70     myView = view;
71     myEditor = view.getEditor();
72     myDocument = myEditor.getDocument();
73   }
74
75   void paint(Graphics2D g) {
76     Rectangle clip = g.getClipBounds();
77     
78     if (myEditor.getContentComponent().isOpaque()) {
79       g.setColor(myEditor.getBackgroundColor());
80       g.fillRect(clip.x, clip.y, clip.width, clip.height);
81     }
82     
83     if (paintPlaceholderText(g)) {
84       paintCaret(g);
85       return;
86     }
87     
88     int startLine = myView.yToVisualLine(clip.y);
89     int endLine = myView.yToVisualLine(clip.y + clip.height);
90     int startOffset = myView.visualLineToOffset(startLine);
91     int endOffset = myView.visualLineToOffset(endLine + 1);
92     ClipDetector clipDetector = new ClipDetector(myEditor, clip);
93     IterationState.CaretData caretData = myEditor.isPaintSelection() ? IterationState.createCaretData(myEditor) : null;
94
95     paintBackground(g, clip, startLine, endLine, caretData);
96     paintRightMargin(g, clip);
97     paintCustomRenderers(g, startOffset, endOffset);
98     MarkupModelEx docMarkup = myEditor.getFilteredDocumentMarkupModel();
99     paintLineMarkersSeparators(g, clip, docMarkup, startOffset, endOffset);
100     paintLineMarkersSeparators(g, clip, myEditor.getMarkupModel(), startOffset, endOffset);
101     paintTextWithEffects(g, clip, startLine, endLine, caretData);
102     paintHighlightersAfterEndOfLine(g, docMarkup, startOffset, endOffset);
103     paintHighlightersAfterEndOfLine(g, myEditor.getMarkupModel(), startOffset, endOffset);
104     paintBorderEffect(g, clipDetector, myEditor.getHighlighter(), startOffset, endOffset);
105     paintBorderEffect(g, clipDetector, docMarkup, startOffset, endOffset);
106     paintBorderEffect(g, clipDetector, myEditor.getMarkupModel(), startOffset, endOffset);
107     
108     paintCaret(g);
109     
110     paintComposedTextDecoration(g);
111   }
112   
113   private boolean paintPlaceholderText(Graphics2D g) {
114     CharSequence hintText = myEditor.getPlaceholder();
115     EditorComponentImpl editorComponent = myEditor.getContentComponent();
116     if (myDocument.getTextLength() > 0 || hintText == null || hintText.length() == 0 ||
117         KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == editorComponent &&
118         !myEditor.getShowPlaceholderWhenFocused()) {
119       return false;
120     }
121   
122     hintText = SwingUtilities.layoutCompoundLabel(g.getFontMetrics(), hintText.toString(), null, 0, 0, 0, 0,
123                                                   SwingUtilities.calculateInnerArea(editorComponent, null), // account for insets
124                                                   new Rectangle(), new Rectangle(), 0);
125     EditorFontType fontType = EditorFontType.PLAIN;
126     Color color = myEditor.getFoldingModel().getPlaceholderAttributes().getForegroundColor();
127     TextAttributes attributes = myEditor.getPlaceholderAttributes();
128     if (attributes != null) {
129       int type = attributes.getFontType();
130       if (type == Font.ITALIC) fontType = EditorFontType.ITALIC;
131       else if (type == Font.BOLD) fontType = EditorFontType.BOLD;
132       else if (type == (Font.ITALIC | Font.BOLD)) fontType = EditorFontType.BOLD_ITALIC;
133
134       Color attColor = attributes.getForegroundColor();
135       if (attColor != null) color = attColor;
136     }
137     g.setColor(color);
138     g.setFont(myEditor.getColorsScheme().getFont(fontType));
139     Insets insets = myView.getInsets();
140     g.drawString(hintText.toString(), insets.left, insets.top + myView.getAscent());
141     return true;
142   }
143   
144   private void paintRightMargin(Graphics g, Rectangle clip) {
145     if (!isRightMarginShown()) return;
146     int x = getRightMarginX();
147     g.setColor(myEditor.getColorsScheme().getColor(EditorColors.RIGHT_MARGIN_COLOR));
148     UIUtil.drawLine(g, x, clip.y, x, clip.y + clip.height);
149   }
150   
151   private boolean isRightMarginShown() {
152     return myEditor.getSettings().isRightMarginShown() && myEditor.getColorsScheme().getColor(EditorColors.RIGHT_MARGIN_COLOR) != null;
153   }
154
155   private int getRightMarginX() {
156     return getMinX() + myEditor.getSettings().getRightMargin(myEditor.getProject()) * myView.getPlainSpaceWidth();
157   }
158   
159   private int getMinX() {
160     return myView.getInsets().left;
161   }
162
163   private void paintBackground(Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine, IterationState.CaretData caretData) {
164     int lineCount = myEditor.getVisibleLineCount();
165     
166     final Map<Integer, Couple<Integer>> virtualSelectionMap = createVirtualSelectionMap(startVisualLine, endVisualLine);
167     final VisualPosition primarySelectionStart = myEditor.getSelectionModel().getSelectionStartPosition();
168     final VisualPosition primarySelectionEnd = myEditor.getSelectionModel().getSelectionEndPosition();
169
170     LineLayout prefixLayout = myView.getPrefixLayout();
171     if (startVisualLine == 0 && prefixLayout != null) {
172       final Insets insets = myView.getInsets();
173       paintBackground(g, myView.getPrefixAttributes(), insets.left, insets.top, prefixLayout.getWidth());
174     }
175
176     VisualLinesIterator visLinesIterator = new VisualLinesIterator(myEditor, startVisualLine);
177     while (!visLinesIterator.atEnd()) {
178       int visualLine = visLinesIterator.getVisualLine();
179       if (visualLine > endVisualLine || visualLine >= lineCount) break;
180       int y = visLinesIterator.getY();
181       paintLineFragments(g, clip, visLinesIterator, caretData, y, new LineFragmentPainter() {
182         @Override
183         public void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
184           paintBackground(g, attributes, getMinX(), y, xEnd);
185           paintSelectionOnSecondSoftWrapLineIfNecessary(g, visualLine, columnEnd, xEnd, y, primarySelectionStart, primarySelectionEnd);
186         }
187
188         @Override
189         public void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end, 
190                           TextAttributes attributes, float xStart, float xEnd, int y) {
191           paintBackground(g, attributes, xStart, y, xEnd - xStart);
192         }
193
194         @Override
195         public void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState it, int columnStart, float x, int y) {
196           paintBackground(g, it.getPastLineEndBackgroundAttributes(), x, y, clip.x + clip.width - x);
197           int offset = it.getEndOffset();
198           SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset);
199           if (softWrap == null) {
200             paintVirtualSelectionIfNecessary(g, visualLine, virtualSelectionMap, columnStart, x, clip.x + clip.width, y);
201           }
202           else {
203             paintSelectionOnFirstSoftWrapLineIfNecessary(g, visualLine, columnStart, x, clip.x + clip.width, y,
204                                                          primarySelectionStart, primarySelectionEnd);
205           }
206         }
207       });
208       visLinesIterator.advance();
209     }
210   }
211
212   private Map<Integer, Couple<Integer>> createVirtualSelectionMap(int startVisualLine, int endVisualLine) {
213     HashMap<Integer, Couple<Integer>> map = new HashMap<>();
214     for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
215       if (caret.hasSelection()) {
216         VisualPosition selectionStart = caret.getSelectionStartPosition();
217         VisualPosition selectionEnd = caret.getSelectionEndPosition();
218         if (selectionStart.line == selectionEnd.line) {
219           int line = selectionStart.line;
220           if (line >= startVisualLine && line <= endVisualLine) {
221             map.put(line, Couple.of(selectionStart.column, selectionEnd.column));
222           }
223         }
224       }
225     }
226     return map;
227   }
228
229   private void paintVirtualSelectionIfNecessary(Graphics2D g,
230                                                 int visualLine,
231                                                 Map<Integer, Couple<Integer>> virtualSelectionMap,
232                                                 int columnStart,
233                                                 float xStart,
234                                                 float xEnd,
235                                                 int y) {
236     Couple<Integer> selectionRange = virtualSelectionMap.get(visualLine);
237     if (selectionRange == null || selectionRange.second <= columnStart) return;
238     float startX = selectionRange.first <= columnStart ? xStart :
239                    (float)myView.visualPositionToXY(new VisualPosition(visualLine, selectionRange.first)).getX();
240     float endX = (float)Math.min(xEnd, myView.visualPositionToXY(new VisualPosition(visualLine, selectionRange.second)).getX());
241     paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);
242   }
243
244   private void paintSelectionOnSecondSoftWrapLineIfNecessary(Graphics2D g, int visualLine, int columnEnd, float xEnd, int y,
245                                                              VisualPosition selectionStartPosition, VisualPosition selectionEndPosition) {
246     if (selectionStartPosition.equals(selectionEndPosition) ||
247         visualLine < selectionStartPosition.line || 
248         visualLine > selectionEndPosition.line || 
249         visualLine == selectionStartPosition.line && selectionStartPosition.column >= columnEnd) {
250       return;
251     }
252
253     float startX = (selectionStartPosition.line == visualLine && selectionStartPosition.column > 0) ?
254                    (float)myView.visualPositionToXY(selectionStartPosition).getX() : getMinX();
255     float endX = (selectionEndPosition.line == visualLine && selectionEndPosition.column < columnEnd) ?
256                  (float)myView.visualPositionToXY(selectionEndPosition).getX() : xEnd;
257     
258     paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);
259   }
260
261   private void paintSelectionOnFirstSoftWrapLineIfNecessary(Graphics2D g, int visualLine, int columnStart, float xStart, float xEnd, int y,
262                                                             VisualPosition selectionStartPosition, VisualPosition selectionEndPosition) {
263     if (selectionStartPosition.equals(selectionEndPosition) ||
264         visualLine < selectionStartPosition.line || 
265         visualLine > selectionEndPosition.line || 
266         visualLine == selectionEndPosition.line && selectionEndPosition.column <= columnStart) {
267       return;
268     }
269
270     float startX = selectionStartPosition.line == visualLine && selectionStartPosition.column > columnStart ?
271                    (float)myView.visualPositionToXY(selectionStartPosition).getX() : xStart;
272     float endX = selectionEndPosition.line == visualLine ?
273                  (float)myView.visualPositionToXY(selectionEndPosition).getX() : xEnd;
274
275     paintBackground(g, myEditor.getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), startX, y, endX - startX);  
276   }
277   
278   private void paintBackground(Graphics2D g, TextAttributes attributes, float x, int y, float width) {
279     if (attributes == null) return;
280     paintBackground(g, attributes.getBackgroundColor(), x, y, width);
281   }
282
283   private void paintBackground(Graphics2D g, Color color, float x, int y, float width) {
284     if (width <= 0 ||
285         color == null ||
286         color.equals(myEditor.getColorsScheme().getDefaultBackground()) ||
287         color.equals(myEditor.getBackgroundColor())) return;
288     g.setColor(color);
289     int xStartRounded = (int)x;
290     int xEndRounded = (int)(x + width);
291     g.fillRect(xStartRounded, y, xEndRounded - xStartRounded, myView.getLineHeight());
292   }
293
294   private void paintCustomRenderers(final Graphics2D g, final int startOffset, final int endOffset) {
295     myEditor.getMarkupModel().processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
296       CustomHighlighterRenderer customRenderer = highlighter.getCustomRenderer();
297       if (customRenderer != null && startOffset < highlighter.getEndOffset() && highlighter.getStartOffset() < endOffset) {
298         customRenderer.paint(myEditor, highlighter, g);
299       }
300       return true;
301     });
302   }
303
304   private void paintLineMarkersSeparators(final Graphics g,
305                                           final Rectangle clip,
306                                           MarkupModelEx markupModel,
307                                           int startOffset,
308                                           int endOffset) {
309     markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
310       paintLineMarkerSeparator(highlighter, clip, g);
311       return true;
312     });
313   }
314
315   private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) {
316     Color separatorColor = marker.getLineSeparatorColor();
317     LineSeparatorRenderer lineSeparatorRenderer = marker.getLineSeparatorRenderer();
318     if (separatorColor == null && lineSeparatorRenderer == null) {
319       return;
320     }
321     int line = myDocument.getLineNumber(marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP
322                                         ? marker.getStartOffset()
323                                         : marker.getEndOffset());
324     int visualLine = myView.logicalToVisualPosition(new LogicalPosition(line + (marker.getLineSeparatorPlacement() == 
325                                                                                 SeparatorPlacement.TOP ? 0 : 1), 0), false).line;
326     int y = myView.visualLineToY(visualLine) - 1;
327     int startX = getMinX();
328     int endX = clip.x + clip.width;
329     if (isRightMarginShown()) {
330       endX = Math.min(endX, getRightMarginX());
331     }
332
333     g.setColor(separatorColor);
334     if (lineSeparatorRenderer != null) {
335       lineSeparatorRenderer.drawLine(g, startX, endX, y);
336     }
337     else {
338       UIUtil.drawLine(g, startX, y, endX, y);
339     }
340   }
341
342
343   private void paintTextWithEffects(Graphics2D g, Rectangle clip, int startVisualLine, int endVisualLine,
344                                     IterationState.CaretData caretData) {
345     final CharSequence text = myDocument.getImmutableCharSequence();
346     final LineWhitespacePaintingStrategy whitespacePaintingStrategy = new LineWhitespacePaintingStrategy(myEditor.getSettings());
347     boolean paintAllSoftWraps = myEditor.getSettings().isAllSoftWrapsShown();
348     int lineCount = myEditor.getVisibleLineCount();
349     final int whiteSpaceStrokeWidth = JBUI.scale(1);
350     final Stroke whiteSpaceStroke = new BasicStroke(whiteSpaceStrokeWidth);
351
352     LineLayout prefixLayout = myView.getPrefixLayout();
353     if (startVisualLine == 0 && prefixLayout != null) {
354       g.setColor(myView.getPrefixAttributes().getForegroundColor());
355       paintLineLayoutWithEffect(g, prefixLayout, getMinX(), myView.getAscent(),
356                                 myView.getPrefixAttributes().getEffectColor(), myView.getPrefixAttributes().getEffectType());
357     }
358
359     VisualLinesIterator visLinesIterator = new VisualLinesIterator(myEditor, startVisualLine);
360     while (!visLinesIterator.atEnd()) {
361       int visualLine = visLinesIterator.getVisualLine();
362       if (visualLine > endVisualLine || visualLine >= lineCount) break;
363
364       int y = visLinesIterator.getY();
365       final boolean paintSoftWraps = paintAllSoftWraps ||
366                                      myEditor.getCaretModel().getLogicalPosition().line == visLinesIterator.getStartLogicalLine();
367       final int[] currentLogicalLine = new int[] {-1}; 
368       
369       paintLineFragments(g, clip, visLinesIterator, caretData, y + myView.getAscent(), new LineFragmentPainter() {
370         @Override
371         public void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y) {
372           if (paintSoftWraps) {
373             SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
374             int symbolWidth = softWrapModel.getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
375             softWrapModel.doPaint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, 
376                                   (int)xEnd - symbolWidth, y - myView.getAscent(), myView.getLineHeight());
377           }
378         }
379
380         @Override
381         public void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end, 
382                           TextAttributes attributes, float xStart, float xEnd, int y) {
383           int lineHeight = myView.getLineHeight();
384           Inlay inlay = fragment.getCurrentInlay();
385           if (inlay != null) {
386             inlay.getRenderer().paint(myEditor, g, 
387                                       new Rectangle((int) xStart, y - myView.getAscent(), inlay.getWidthInPixels(), lineHeight));
388             return;
389           }
390           boolean allowBorder = fragment.getCurrentFoldRegion() != null;
391           if (attributes != null && hasTextEffect(attributes.getEffectColor(), attributes.getEffectType(), allowBorder)) {
392             paintTextEffect(g, xStart, xEnd, y, attributes.getEffectColor(), attributes.getEffectType(), allowBorder);
393           }
394           if (attributes != null && attributes.getForegroundColor() != null) {
395             g.setColor(attributes.getForegroundColor());
396             fragment.draw(g, xStart, y, start, end);
397           }
398           if (fragment.getCurrentFoldRegion() == null) {
399             int logicalLine = fragment.getStartLogicalLine();
400             if (logicalLine != currentLogicalLine[0]) {
401               whitespacePaintingStrategy.update(text, myDocument.getLineStartOffset(logicalLine), myDocument.getLineEndOffset(logicalLine));
402               currentLogicalLine[0] = logicalLine;
403             }
404             paintWhitespace(g, text, xStart, y, start, end, whitespacePaintingStrategy, fragment, whiteSpaceStroke, whiteSpaceStrokeWidth);
405           }
406         }
407
408         @Override
409         public void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState iterationState, int columnStart, float x, int y) {
410           int offset = iterationState.getEndOffset();
411           SoftWrapModelImpl softWrapModel = myEditor.getSoftWrapModel();
412           if (softWrapModel.getSoftWrap(offset) == null) {
413             int logicalLine = myDocument.getLineNumber(offset);
414             paintLineExtensions(g, logicalLine, x, y);
415           }
416           else if (paintSoftWraps) {
417             softWrapModel.doPaint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, 
418                                   (int)x, y - myView.getAscent(), myView.getLineHeight());
419           }
420         }
421       });
422       visLinesIterator.advance();
423     }
424     ComplexTextFragment.flushDrawingCache(g);
425   }
426
427   private float paintLineLayoutWithEffect(Graphics2D g, LineLayout layout, float x, float y, 
428                                   @Nullable Color effectColor, @Nullable EffectType effectType) {
429     if (hasTextEffect(effectColor, effectType, false)) {
430       paintTextEffect(g, x, x + layout.getWidth(), (int)y, effectColor, effectType, false);
431     }
432     for (LineLayout.VisualFragment fragment : layout.getFragmentsInVisualOrder(x)) {
433       fragment.draw(g, x, y);
434       x = fragment.getEndX();
435     }
436     return x;
437   }
438
439   private static boolean hasTextEffect(@Nullable Color effectColor, @Nullable EffectType effectType, boolean allowBorder) {
440     return effectColor != null && (effectType == EffectType.LINE_UNDERSCORE ||
441                                    effectType == EffectType.BOLD_LINE_UNDERSCORE ||
442                                    effectType == EffectType.BOLD_DOTTED_LINE ||
443                                    effectType == EffectType.WAVE_UNDERSCORE ||
444                                    effectType == EffectType.STRIKEOUT ||
445                                    allowBorder && (effectType == EffectType.BOXED || effectType == EffectType.ROUNDED_BOX));
446   }
447
448   private void paintTextEffect(Graphics2D g, float xFrom, float xTo, int y, Color effectColor, EffectType effectType, boolean allowBorder) {
449     g.setColor(effectColor);
450     int xStart = (int)xFrom;
451     int xEnd = (int)xTo;
452     if (effectType == EffectType.LINE_UNDERSCORE) {
453       EffectPainter.LINE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
454                                           myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
455     }
456     else if (effectType == EffectType.BOLD_LINE_UNDERSCORE) {
457       EffectPainter.BOLD_LINE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
458                                                myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
459     }
460     else if (effectType == EffectType.STRIKEOUT) {
461       EffectPainter.STRIKE_THROUGH.paint(g, xStart, y, xEnd - xStart, myView.getCharHeight(),
462                                          myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
463     }
464     else if (effectType == EffectType.WAVE_UNDERSCORE) {
465       EffectPainter.WAVE_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
466                                           myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
467     }
468     else if (effectType == EffectType.BOLD_DOTTED_LINE) {
469       EffectPainter.BOLD_DOTTED_UNDERSCORE.paint(g, xStart, y, xEnd - xStart, myView.getDescent(),
470                                                  myEditor.getColorsScheme().getFont(EditorFontType.PLAIN));
471     }
472     else if (allowBorder && (effectType == EffectType.BOXED || effectType == EffectType.ROUNDED_BOX)) {
473       drawSimpleBorder(g, xFrom, xTo, y - myView.getAscent(), effectType == EffectType.ROUNDED_BOX);
474     }
475   }
476
477   private void paintWhitespace(Graphics2D g, CharSequence text, float x, int y, int start, int end,
478                                LineWhitespacePaintingStrategy whitespacePaintingStrategy,
479                                VisualLineFragmentsIterator.Fragment fragment, Stroke stroke, int strokeWidth) {
480     Stroke oldStroke = g.getStroke();
481     try {
482       g.setColor(myEditor.getColorsScheme().getColor(EditorColors.WHITESPACES_COLOR));
483       g.setStroke(stroke); // applied for tab & ideographic space
484
485       boolean isRtl = fragment.isRtl();
486       int baseStartOffset = fragment.getStartOffset();
487       int startOffset = isRtl ? baseStartOffset - start : baseStartOffset + start;
488       y -= 1;
489
490       for (int i = start; i < end; i++) {
491         int charOffset = isRtl ? baseStartOffset - i - 1 : baseStartOffset + i;
492         char c = text.charAt(charOffset);
493         if (" \t\u3000".indexOf(c) >= 0 && whitespacePaintingStrategy.showWhitespaceAtOffset(charOffset)) {
494           int startX = (int)fragment.offsetToX(x, startOffset, isRtl ? baseStartOffset - i : baseStartOffset + i);
495           int endX = (int)fragment.offsetToX(x, startOffset, isRtl ? baseStartOffset - i - 1 : baseStartOffset + i + 1);
496
497           if (c == ' ') {
498             //noinspection SuspiciousNameCombination
499             g.fillRect((startX + endX - strokeWidth) / 2, y - strokeWidth + 1, strokeWidth, strokeWidth);
500           }
501           else if (c == '\t') {
502             endX -= myView.getPlainSpaceWidth() / 4;
503             int height = myView.getCharHeight();
504             int halfHeight = height / 2;
505             int mid = y - halfHeight;
506             int top = y - height;
507             UIUtil.drawLine(g, startX, mid, endX, mid);
508             UIUtil.drawLine(g, endX, y, endX, top);
509             g.fillPolygon(new int[]{endX - halfHeight, endX - halfHeight, endX}, new int[]{y, y - height, y - halfHeight}, 3);
510           }
511           else if (c == '\u3000') { // ideographic space
512             int charHeight = myView.getCharHeight();
513             g.drawRect(startX + JBUI.scale(2) + strokeWidth/2, y - charHeight + strokeWidth/2,
514                        endX - startX - JBUI.scale(4) - (strokeWidth - 1), charHeight - (strokeWidth - 1));
515           }
516         }
517       }
518     } finally {
519       g.setStroke(oldStroke);
520     }
521   }
522
523   private void paintLineExtensions(Graphics2D g, int line, float x, int y) {
524     Project project = myEditor.getProject();
525     VirtualFile virtualFile = myEditor.getVirtualFile();
526     if (project == null || virtualFile == null) return;
527     for (EditorLinePainter painter : EditorLinePainter.EP_NAME.getExtensions()) {
528       Collection<LineExtensionInfo> extensions = painter.getLineExtensions(project, virtualFile, line);
529       if (extensions != null) {
530         for (LineExtensionInfo info : extensions) {
531           LineLayout layout = LineLayout.create(myView, info.getText(), info.getFontType());
532           g.setColor(info.getColor());
533           x = paintLineLayoutWithEffect(g, layout, x, y, info.getEffectColor(), info.getEffectType());
534           int currentLineWidth = (int)x - getMinX();
535           EditorSizeManager sizeManager = myView.getSizeManager();
536           if (currentLineWidth > sizeManager.getMaxLineWithExtensionWidth()) {
537             sizeManager.setMaxLineWithExtensionWidth(line, currentLineWidth);
538           }
539         }
540       }
541     }
542   }
543
544   private void paintHighlightersAfterEndOfLine(final Graphics2D g,
545                                                MarkupModelEx markupModel,
546                                                final int startOffset,
547                                                int endOffset) {
548     markupModel.processRangeHighlightersOverlappingWith(startOffset, endOffset, highlighter -> {
549       if (highlighter.getStartOffset() >= startOffset) {
550         paintHighlighterAfterEndOfLine(g, highlighter);
551       }
552       return true;
553     });
554   }
555
556   private void paintHighlighterAfterEndOfLine(Graphics2D g, RangeHighlighterEx highlighter) {
557     if (!highlighter.isAfterEndOfLine()) {
558       return;
559     }
560     int startOffset = highlighter.getStartOffset();
561     int lineEndOffset = myDocument.getLineEndOffset(myDocument.getLineNumber(startOffset));
562     if (myEditor.getFoldingModel().isOffsetCollapsed(lineEndOffset)) return;
563     Point2D lineEnd = myView.offsetToXY(lineEndOffset, true, false);
564     float x = (float)lineEnd.getX();
565     int y = (int)lineEnd.getY();
566     TextAttributes attributes = highlighter.getTextAttributes();
567     paintBackground(g, attributes, x, y, myView.getPlainSpaceWidth());
568     if (attributes != null && hasTextEffect(attributes.getEffectColor(), attributes.getEffectType(), false)) {
569       paintTextEffect(g, x, x + myView.getPlainSpaceWidth() - 1, y + myView.getAscent(), 
570                       attributes.getEffectColor(), attributes.getEffectType(), false);
571     }
572   }
573
574   private void paintBorderEffect(Graphics2D g,
575                                  ClipDetector clipDetector,
576                                  EditorHighlighter highlighter,
577                                  int clipStartOffset,
578                                  int clipEndOffset) {
579     HighlighterIterator it = highlighter.createIterator(clipStartOffset);
580     while (!it.atEnd() && it.getStart() < clipEndOffset) {
581       TextAttributes attributes = it.getTextAttributes();
582       if (isBorder(attributes)) {
583         paintBorderEffect(g, clipDetector, it.getStart(), it.getEnd(), attributes);
584       }
585       it.advance();
586     }
587   }
588
589   private void paintBorderEffect(final Graphics2D g,
590                                  final ClipDetector clipDetector,
591                                  MarkupModelEx markupModel,
592                                  int clipStartOffset,
593                                  int clipEndOffset) {
594     markupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, rangeHighlighter -> {
595       TextAttributes attributes = rangeHighlighter.getTextAttributes();
596       if (isBorder(attributes)) {
597         paintBorderEffect(g, clipDetector, rangeHighlighter.getAffectedAreaStartOffset(), rangeHighlighter.getAffectedAreaEndOffset(),
598                           attributes);
599       }
600       return true;
601     });
602   }
603
604   private static boolean isBorder(TextAttributes attributes) {
605     return attributes != null &&
606            (attributes.getEffectType() == EffectType.BOXED || attributes.getEffectType() == EffectType.ROUNDED_BOX) &&
607            attributes.getEffectColor() != null;
608   }
609
610   private void paintBorderEffect(Graphics2D g, ClipDetector clipDetector, int startOffset, int endOffset, TextAttributes attributes) {
611     startOffset = DocumentUtil.alignToCodePointBoundary(myDocument, startOffset);
612     endOffset = DocumentUtil.alignToCodePointBoundary(myDocument, endOffset);
613     if (!clipDetector.rangeCanBeVisible(startOffset, endOffset)) return;
614     int startLine = myDocument.getLineNumber(startOffset);
615     int endLine = myDocument.getLineNumber(endOffset);
616     if (startLine + 1 == endLine &&
617         startOffset == myDocument.getLineStartOffset(startLine) &&
618         endOffset == myDocument.getLineStartOffset(endLine)) {
619       // special case of line highlighters
620       endLine--;
621       endOffset = myDocument.getLineEndOffset(endLine);
622     }
623   
624     boolean rounded = attributes.getEffectType() == EffectType.ROUNDED_BOX;
625     g.setColor(attributes.getEffectColor());
626     VisualPosition startPosition = myView.offsetToVisualPosition(startOffset, true, false);
627     VisualPosition endPosition = myView.offsetToVisualPosition(endOffset, false, true);
628     if (startPosition.line == endPosition.line) {
629       int y = myView.visualLineToY(startPosition.line);
630       TFloatArrayList ranges = adjustedLogicalRangeToVisualRanges(startOffset, endOffset);
631       for (int i = 0; i < ranges.size() - 1; i+= 2) {
632         float startX = ranges.get(i);
633         float endX = ranges.get(i + 1);
634         drawSimpleBorder(g, startX, endX + 1, y, rounded);
635       }
636     }
637     else {
638       TFloatArrayList leadingRanges = adjustedLogicalRangeToVisualRanges(
639         startOffset, myView.visualPositionToOffset(new VisualPosition(startPosition.line, Integer.MAX_VALUE, true)));
640       TFloatArrayList trailingRanges = adjustedLogicalRangeToVisualRanges(
641         myView.visualPositionToOffset(new VisualPosition(endPosition.line, 0)), endOffset);
642       if (!leadingRanges.isEmpty() && !trailingRanges.isEmpty()) {
643         int minX = getMinX();
644         int maxX = Math.max(minX + myView.getMaxWidthInLineRange(startPosition.line, endPosition.line - 1) - 1,
645                             (int)trailingRanges.get(trailingRanges.size() - 1));
646         boolean containsInnerLines = endPosition.line > startPosition.line + 1;
647         int lineHeight = myView.getLineHeight() - 1;
648         int leadingTopY = myView.visualLineToY(startPosition.line);
649         int leadingBottomY = leadingTopY + lineHeight;
650         int trailingTopY = myView.visualLineToY(endPosition.line);
651         int trailingBottomY = trailingTopY + lineHeight;
652         float start = 0;
653         float end = 0;
654         float leftGap = leadingRanges.get(0) - (containsInnerLines ? minX : trailingRanges.get(0));
655         int adjustY = leftGap == 0 ? 2 : leftGap > 0 ? 1 : 0; // avoiding 1-pixel gap between aligned lines
656         for (int i = 0; i < leadingRanges.size() - 1; i += 2) {
657           start = leadingRanges.get(i);
658           end = leadingRanges.get(i + 1);
659           if (i > 0) {
660             drawLine(g, leadingRanges.get(i - 1), leadingBottomY, start, leadingBottomY, rounded);
661           }
662           drawLine(g, start, leadingBottomY + (i == 0 ? adjustY : 0), start, leadingTopY, rounded);
663           if ((i + 2) < leadingRanges.size()) {
664             drawLine(g, start, leadingTopY, end, leadingTopY, rounded);
665             drawLine(g, end, leadingTopY, end, leadingBottomY, rounded);
666           }
667         }
668         end = Math.max(end, maxX);
669         drawLine(g, start, leadingTopY, end, leadingTopY, rounded);
670         drawLine(g, end, leadingTopY, end, trailingTopY - 1, rounded);
671         float targetX = trailingRanges.get(trailingRanges.size() - 1);
672         drawLine(g, end, trailingTopY - 1, targetX, trailingTopY - 1, rounded);
673         adjustY = end == targetX ? -2 : -1; // for lastX == targetX we need to avoid a gap when rounding is used
674         for (int i = trailingRanges.size() - 2; i >= 0; i -= 2) {
675           start = trailingRanges.get(i);
676           end = trailingRanges.get(i + 1);
677
678           drawLine(g, end, trailingTopY + (i == 0 ? adjustY : 0), end, trailingBottomY, rounded);
679           drawLine(g, end, trailingBottomY, start, trailingBottomY, rounded);
680           drawLine(g, start, trailingBottomY, start, trailingTopY, rounded);
681           if (i > 0) {
682             drawLine(g, start, trailingTopY, trailingRanges.get(i - 1), trailingTopY, rounded);
683           }
684         }
685         float lastX = start;
686         if (containsInnerLines) {
687           if (start > minX) {
688             drawLine(g, start, trailingTopY, start, trailingTopY - 1, rounded);
689             drawLine(g, start, trailingTopY - 1, minX, trailingTopY - 1, rounded);
690             drawLine(g, minX, trailingTopY - 1, minX, leadingBottomY + 1, rounded);
691           }
692           else {
693             drawLine(g, minX, trailingTopY, minX, leadingBottomY + 1, rounded);
694           }
695           lastX = minX;
696         }
697         targetX = leadingRanges.get(0);
698         if (lastX < targetX) {
699           drawLine(g, lastX, leadingBottomY + 1, targetX, leadingBottomY + 1, rounded);
700         }
701         else {
702           drawLine(g, lastX, leadingBottomY + 1, lastX, leadingBottomY, rounded);
703           drawLine(g, lastX, leadingBottomY, targetX, leadingBottomY, rounded);
704         }
705       }
706     }
707   }
708
709   private void drawSimpleBorder(Graphics2D g, float xStart, float xEnd, float y, boolean rounded) {
710     Shape border = getBorderShape(xStart, y, xEnd - xStart, myView.getLineHeight(), rounded);
711     if (border != null) {
712       Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
713       g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
714       g.fill(border);
715       g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
716     }
717   }
718
719   private static Shape getBorderShape(float x, float y, float width, int height, boolean rounded) {
720     if (width <= 0 || height <= 0) return null;
721     Shape outer = rounded
722                   ? new RoundRectangle2D.Float(x, y, width, height, 2, 2)
723                   : new Rectangle2D.Float(x, y, width, height);
724
725     if (width <= 2 || height <= 2) return outer;
726     Shape inner = new Rectangle2D.Float(x + 1, y + 1, width - 2, height - 2);
727
728     Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
729     path.append(outer, false);
730     path.append(inner, false);
731     return path;
732   }
733
734   private static void drawLine(Graphics2D g, float x1, int y1, float x2, int y2, boolean rounded) {
735     if (rounded) {
736       UIUtil.drawLinePickedOut(g, (int) x1, y1, (int)x2, y2);
737     } else {
738       UIUtil.drawLine(g, (int)x1, y1, (int)x2, y2);
739     }
740   }
741
742   /**
743    * Returns ranges obtained from {@link #logicalRangeToVisualRanges(int, int)}, adjusted for painting range border - lines should
744    * line inside target ranges (except for empty range). Target offsets are supposed to be located on the same visual line.
745    */
746   private TFloatArrayList adjustedLogicalRangeToVisualRanges(int startOffset, int endOffset) {
747     TFloatArrayList ranges = logicalRangeToVisualRanges(startOffset, endOffset);
748     for (int i = 0; i < ranges.size() - 1; i += 2) {
749       float startX = ranges.get(i);
750       float endX = ranges.get(i + 1);
751       if (startX == endX) {
752         if (startX > 0) {
753           startX--;
754         }
755         else {
756           endX++;
757         }
758       }
759       else {
760         endX--;
761       }
762       ranges.set(i, startX);
763       ranges.set(i + 1, endX);
764     }
765     return ranges;
766   }
767
768
769     /**
770      * Returns a list of pairs of x coordinates for visual ranges representing given logical range. If 
771      * {@code startOffset == endOffset}, a pair of equal numbers is returned, corresponding to target position. Target offsets are
772      * supposed to be located on the same visual line.
773      */
774   private TFloatArrayList logicalRangeToVisualRanges(int startOffset, int endOffset) {
775     assert startOffset <= endOffset;
776     TFloatArrayList result = new TFloatArrayList();
777     if (myDocument.getTextLength() == 0) {
778       int minX = getMinX();
779       result.add(minX);
780       result.add(minX);
781     }
782     else {
783       for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, startOffset, false)) {
784         int minOffset = fragment.getMinOffset();
785         int maxOffset = fragment.getMaxOffset();
786         if (startOffset == endOffset) {
787           if (startOffset >= minOffset && startOffset <= maxOffset) {
788             float x = fragment.offsetToX(startOffset);
789             result.add(x);
790             result.add(x);
791             break;
792           }
793         }
794         else if (startOffset < maxOffset && endOffset > minOffset) {
795           float x1 = minOffset == maxOffset ? fragment.getStartX() : fragment.offsetToX(Math.max(minOffset, startOffset));
796           float x2 = minOffset == maxOffset ? fragment.getEndX() : fragment.offsetToX(Math.min(maxOffset, endOffset));
797           if (x1 > x2) {
798             float tmp = x1;
799             x1 = x2;
800             x2 = tmp;
801           }
802           if (result.isEmpty() || x1 > result.get(result.size() - 1)) {
803             result.add(x1);
804             result.add(x2);
805           }
806           else {
807             result.set(result.size() - 1, x2);
808           }
809         }
810       }
811     }
812     return result;
813   } 
814
815   private void paintComposedTextDecoration(Graphics2D g) {
816     TextRange composedTextRange = myEditor.getComposedTextRange();
817     if (composedTextRange != null) {
818       Point2D p1 = myView.offsetToXY(Math.min(composedTextRange.getStartOffset(), myDocument.getTextLength()), true, false);
819       Point2D p2 = myView.offsetToXY(Math.min(composedTextRange.getEndOffset(), myDocument.getTextLength()), false, true);
820   
821       int y = (int)p1.getY() + myView.getAscent() + 1;
822      
823       g.setStroke(IME_COMPOSED_TEXT_UNDERLINE_STROKE);
824       g.setColor(myEditor.getColorsScheme().getDefaultForeground());
825       UIUtil.drawLine(g, (int)p1.getX(), y, (int)p2.getX(), y);
826     }
827   }
828
829   private void paintCaret(Graphics2D g_) {
830     EditorImpl.CaretRectangle[] locations = myEditor.getCaretLocations(true);
831     if (locations == null) return;
832
833     Graphics2D g = IdeBackgroundUtil.getOriginalGraphics(g_);
834     int nominalLineHeight = myView.getNominalLineHeight();
835     int topOverhang = myView.getTopOverhang();
836     EditorSettings settings = myEditor.getSettings();
837     Color caretColor = myEditor.getColorsScheme().getColor(EditorColors.CARET_COLOR);
838     if (caretColor == null) caretColor = new JBColor(CARET_DARK, CARET_LIGHT);
839     int minX = getMinX();
840     for (EditorImpl.CaretRectangle location : locations) {
841       float x = location.myPoint.x;
842       int y = location.myPoint.y - topOverhang;
843       Caret caret = location.myCaret;
844       CaretVisualAttributes attr = caret == null ? CaretVisualAttributes.DEFAULT : caret.getVisualAttributes();
845       g.setColor(attr.getColor() != null ? attr.getColor() : caretColor);
846       boolean isRtl = location.myIsRtl;
847       if (myEditor.isInsertMode() != settings.isBlockCursor()) {
848         int lineWidth = JBUI.scale(attr.getWidth(settings.getLineCursorWidth()));
849         // fully cover extra character's pixel which can appear due to antialiasing
850         // see IDEA-148843 for more details
851         if (x > minX && lineWidth > 1) x -= 1 / JBUI.sysScale(g);
852         g.fill(new Rectangle2D.Float(x, y, lineWidth, nominalLineHeight));
853         if (myDocument.getTextLength() > 0 && caret != null &&
854             !myView.getTextLayoutCache().getLineLayout(caret.getLogicalPosition().line).isLtr()) {
855           GeneralPath triangle = new GeneralPath(Path2D.WIND_NON_ZERO, 3);
856           triangle.moveTo(isRtl ? x + lineWidth : x, y);
857           triangle.lineTo(isRtl ? x + lineWidth - CARET_DIRECTION_MARK_SIZE : x + CARET_DIRECTION_MARK_SIZE, y);
858           triangle.lineTo(isRtl ? x + lineWidth : x, y + CARET_DIRECTION_MARK_SIZE);
859           triangle.closePath();
860           g.fill(triangle);
861         }
862       }
863       else {
864         int width = location.myWidth;
865         float startX = Math.max(minX, isRtl ? x - width : x);
866         g.fill(new Rectangle2D.Float(startX, y, width, nominalLineHeight - 1));
867         if (myDocument.getTextLength() > 0 && caret != null) {
868           int charCount = DocumentUtil.isSurrogatePair(myDocument, caret.getOffset()) ? 2 : 1;
869           int targetVisualColumn = caret.getVisualPosition().column;
870           for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView,
871                                                                                                   caret.getVisualLineStart(), 
872                                                                                                   false)) {
873             if (fragment.getCurrentInlay() != null) continue;
874             int startVisualColumn = fragment.getStartVisualColumn();
875             int endVisualColumn = fragment.getEndVisualColumn();
876             if (startVisualColumn < targetVisualColumn && endVisualColumn > targetVisualColumn ||
877                 startVisualColumn == targetVisualColumn && !isRtl ||
878                 endVisualColumn == targetVisualColumn && isRtl) {
879               g.setColor(ColorUtil.isDark(caretColor) ? CARET_LIGHT : CARET_DARK);
880               fragment.draw(g, startX, y + topOverhang + myView.getAscent(),
881                             targetVisualColumn - startVisualColumn - (isRtl ? charCount : 0),
882                             targetVisualColumn - startVisualColumn + (isRtl ? 0 : charCount));
883               break;
884             }
885           }
886           ComplexTextFragment.flushDrawingCache(g);
887         }
888       }
889     }
890   }
891   
892   void repaintCarets() {
893     EditorImpl.CaretRectangle[] locations = myEditor.getCaretLocations(false);
894     if (locations == null) return;
895     int nominalLineHeight = myView.getNominalLineHeight();
896     int topOverhang = myView.getTopOverhang();
897     for (EditorImpl.CaretRectangle location : locations) {
898       int x = location.myPoint.x;
899       int y = location.myPoint.y - topOverhang;
900       int width = Math.max(location.myWidth, CARET_DIRECTION_MARK_SIZE);
901       myEditor.getContentComponent().repaintEditorComponent(x - width, y, width * 2, nominalLineHeight);
902     }
903   }
904
905   private void paintLineFragments(Graphics2D g, Rectangle clip, VisualLinesIterator visLineIterator, IterationState.CaretData caretData,
906                                   int y, LineFragmentPainter painter) {
907     int visualLine = visLineIterator.getVisualLine();
908     float x = getMinX() + (visualLine == 0 ? myView.getPrefixTextWidthInPixels() : 0);
909     int offset = visLineIterator.getVisualLineStartOffset();
910     int visualLineEndOffset = visLineIterator.getVisualLineEndOffset();
911     IterationState it = null;
912     int prevEndOffset = -1;
913     boolean firstFragment = true;
914     int maxColumn = 0;
915     for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visLineIterator, null)) {
916       int fragmentStartOffset = fragment.getStartOffset();
917       int start = fragmentStartOffset;
918       int end = fragment.getEndOffset();
919       x = fragment.getStartX();
920       if (firstFragment) {
921         firstFragment = false;
922         SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset);
923         if (softWrap != null) {
924           prevEndOffset = offset;
925           it = new IterationState(myEditor, offset == 0 ? 0 : DocumentUtil.getPreviousCodePointOffset(myDocument, offset), visualLineEndOffset,
926                                   caretData, false, false, false, false);
927           if (it.getEndOffset() <= offset) {
928             it.advance();
929           }
930           if (x >= clip.getMinX()) {
931             painter.paintBeforeLineStart(g, it.getStartOffset() == offset ? it.getBeforeLineStartBackgroundAttributes() :
932                                             it.getMergedAttributes(), fragment.getStartVisualColumn(), x, y);
933           }
934         }
935       }
936       FoldRegion foldRegion = fragment.getCurrentFoldRegion();
937       if (foldRegion == null) {
938         if (start != prevEndOffset) {
939           it = new IterationState(myEditor, start, fragment.isRtl() ? offset : visualLineEndOffset, 
940                                   caretData, false, false, false, fragment.isRtl());
941         }
942         prevEndOffset = end;
943         assert it != null;
944         if (start == end) { // special case of inlays
945           if (start == it.getEndOffset() && !it.atEnd()) {
946             it.advance();
947           }
948           TextAttributes attributes = it.getStartOffset() == start ? it.getBreakAttributes() : it.getMergedAttributes();
949           float xNew = fragment.getEndX();
950           if (xNew >= clip.getMinX()) {
951             painter.paint(g, fragment, 0, 0, attributes, x, xNew, y);
952           }
953           x = xNew;
954         }
955         else {
956           while (fragment.isRtl() ? start > end : start < end) {
957             if (fragment.isRtl() ? it.getEndOffset() >= start : it.getEndOffset() <= start) {
958               assert !it.atEnd();
959               it.advance();
960             }
961             TextAttributes attributes = it.getMergedAttributes();
962             int curEnd = fragment.isRtl() ? Math.max(it.getEndOffset(), end) : Math.min(it.getEndOffset(), end);
963             float xNew = fragment.offsetToX(x, start, curEnd);
964             if (xNew >= clip.getMinX()) {
965               painter.paint(g, fragment,
966                             fragment.isRtl() ? fragmentStartOffset - start : start - fragmentStartOffset,
967                             fragment.isRtl() ? fragmentStartOffset - curEnd : curEnd - fragmentStartOffset,
968                             attributes, x, xNew, y);
969             }
970             x = xNew;
971             start = curEnd;
972           }
973         }
974       }
975       else {
976         float xNew = fragment.getEndX();
977         if (xNew >= clip.getMinX()) {
978           painter.paint(g, fragment, 0, fragment.getEndVisualColumn() - fragment.getStartVisualColumn(), 
979                         getFoldRegionAttributes(foldRegion), x, xNew, y);
980         }
981         x = xNew;
982         prevEndOffset = -1;
983         it = null;
984       }
985       if (x > clip.getMaxX()) return;
986       maxColumn = fragment.getEndVisualColumn();
987     }
988     if (it == null || it.getEndOffset() != visualLineEndOffset) {
989       it = new IterationState(myEditor, visualLineEndOffset == offset ? visualLineEndOffset
990                                                                       : DocumentUtil.getPreviousCodePointOffset(myDocument, visualLineEndOffset),
991                               visualLineEndOffset, caretData, false, false, false, false);
992     }
993     if (!it.atEnd()) {
994       it.advance();
995     }
996     assert it.atEnd();
997     painter.paintAfterLineEnd(g, clip, it, maxColumn, x, y);
998   }
999
1000   private TextAttributes getFoldRegionAttributes(FoldRegion foldRegion) {
1001     TextAttributes selectionAttributes = isSelected(foldRegion) ? myEditor.getSelectionModel().getTextAttributes() : null;
1002     TextAttributes foldAttributes = myEditor.getFoldingModel().getPlaceholderAttributes();
1003     TextAttributes defaultAttributes = getDefaultAttributes();
1004     return mergeAttributes(mergeAttributes(selectionAttributes, foldAttributes), defaultAttributes);
1005   }
1006
1007   @SuppressWarnings("UseJBColor")
1008   private TextAttributes getDefaultAttributes() {
1009     TextAttributes attributes = myEditor.getColorsScheme().getAttributes(HighlighterColors.TEXT);
1010     if (attributes.getForegroundColor() == null) attributes.setForegroundColor(Color.black);
1011     if (attributes.getBackgroundColor() == null) attributes.setBackgroundColor(Color.white);
1012     return attributes;
1013   }
1014
1015   private static boolean isSelected(FoldRegion foldRegion) {
1016     int regionStart = foldRegion.getStartOffset();
1017     int regionEnd = foldRegion.getEndOffset();
1018     int[] selectionStarts = foldRegion.getEditor().getSelectionModel().getBlockSelectionStarts();
1019     int[] selectionEnds = foldRegion.getEditor().getSelectionModel().getBlockSelectionEnds();
1020     for (int i = 0; i < selectionStarts.length; i++) {
1021       int start = selectionStarts[i];
1022       int end = selectionEnds[i];
1023       if (regionStart >= start && regionEnd <= end) return true;
1024     }
1025     return false;
1026   }
1027
1028   private static TextAttributes mergeAttributes(TextAttributes primary, TextAttributes secondary) {
1029     if (primary == null) return secondary;
1030     if (secondary == null) return primary;
1031     return new TextAttributes(primary.getForegroundColor() == null ? secondary.getForegroundColor() : primary.getForegroundColor(),
1032                               primary.getBackgroundColor() == null ? secondary.getBackgroundColor() : primary.getBackgroundColor(),
1033                               primary.getEffectColor() == null ? secondary.getEffectColor() : primary.getEffectColor(),
1034                               primary.getEffectType() == null ? secondary.getEffectType() : primary.getEffectType(),
1035                               primary.getFontType() == Font.PLAIN ? secondary.getFontType() : primary.getFontType());
1036   }
1037
1038   @Override
1039   public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, FontInfo fontInfo) {
1040     g.setFont(fontInfo.getFont());
1041     g.setColor(color);
1042     g.drawChars(data, start, end - start, x, y);
1043   }
1044
1045   interface LineFragmentPainter {
1046     void paintBeforeLineStart(Graphics2D g, TextAttributes attributes, int columnEnd, float xEnd, int y);
1047     void paint(Graphics2D g, VisualLineFragmentsIterator.Fragment fragment, int start, int end, TextAttributes attributes,
1048                float xStart, float xEnd, int y);
1049     void paintAfterLineEnd(Graphics2D g, Rectangle clip, IterationState iterationState, int columnStart, float x, int y);
1050   }
1051
1052   private static class LineWhitespacePaintingStrategy {
1053     private final boolean myWhitespaceShown;
1054     private final boolean myLeadingWhitespaceShown;
1055     private final boolean myInnerWhitespaceShown;
1056     private final boolean myTrailingWhitespaceShown;
1057
1058     // Offsets on current line where leading whitespace ends and trailing whitespace starts correspondingly.
1059     private int currentLeadingEdge;
1060     private int currentTrailingEdge;
1061
1062     public LineWhitespacePaintingStrategy(EditorSettings settings) {
1063       myWhitespaceShown = settings.isWhitespacesShown();
1064       myLeadingWhitespaceShown = settings.isLeadingWhitespaceShown();
1065       myInnerWhitespaceShown = settings.isInnerWhitespaceShown();
1066       myTrailingWhitespaceShown = settings.isTrailingWhitespaceShown();
1067     }
1068
1069     private void update(CharSequence chars, int lineStart, int lineEnd) {
1070       if (myWhitespaceShown
1071           && (myLeadingWhitespaceShown || myInnerWhitespaceShown || myTrailingWhitespaceShown)
1072           && !(myLeadingWhitespaceShown && myInnerWhitespaceShown && myTrailingWhitespaceShown)) {
1073         currentTrailingEdge = CharArrayUtil.shiftBackward(chars, lineStart, lineEnd - 1, WHITESPACE_CHARS) + 1;
1074         currentLeadingEdge = CharArrayUtil.shiftForward(chars, lineStart, currentTrailingEdge, WHITESPACE_CHARS);
1075       }
1076     }
1077
1078     private boolean showWhitespaceAtOffset(int offset) {
1079       return myWhitespaceShown
1080              && (offset < currentLeadingEdge ? myLeadingWhitespaceShown :
1081                  offset >= currentTrailingEdge ? myTrailingWhitespaceShown :
1082                  myInnerWhitespaceShown);
1083     }
1084   }
1085 }