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