Merge branch 'master' of git.labs.intellij.net:idea/community
[idea/community.git] / platform / lang-impl / src / com / intellij / openapi / editor / richcopy / TextWithMarkupProcessor.java
1 /*
2  * Copyright 2000-2014 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.richcopy;
17
18 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
19 import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
20 import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
21 import com.intellij.codeInsight.editorActions.CopyPastePreProcessor;
22 import com.intellij.ide.highlighter.HighlighterFactory;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.editor.*;
26 import com.intellij.openapi.editor.colors.EditorColorsScheme;
27 import com.intellij.openapi.editor.colors.FontPreferences;
28 import com.intellij.openapi.editor.colors.TextAttributesKey;
29 import com.intellij.openapi.editor.ex.DisposableIterator;
30 import com.intellij.openapi.editor.ex.MarkupModelEx;
31 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
32 import com.intellij.openapi.editor.ex.util.EditorUtil;
33 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
34 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
35 import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry;
36 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
37 import com.intellij.openapi.editor.impl.FontInfo;
38 import com.intellij.openapi.editor.markup.HighlighterLayer;
39 import com.intellij.openapi.editor.markup.MarkupModel;
40 import com.intellij.openapi.editor.markup.TextAttributes;
41 import com.intellij.openapi.editor.richcopy.model.SyntaxInfo;
42 import com.intellij.openapi.editor.richcopy.settings.RichCopySettings;
43 import com.intellij.openapi.editor.richcopy.view.HtmlTransferableData;
44 import com.intellij.openapi.editor.richcopy.view.RawTextWithMarkup;
45 import com.intellij.openapi.editor.richcopy.view.RtfTransferableData;
46 import com.intellij.openapi.project.Project;
47 import com.intellij.openapi.util.Pair;
48 import com.intellij.openapi.util.Ref;
49 import com.intellij.openapi.util.SystemInfo;
50 import com.intellij.openapi.util.registry.Registry;
51 import com.intellij.psi.PsiFile;
52 import com.intellij.psi.TokenType;
53 import com.intellij.util.ObjectUtils;
54 import com.intellij.util.text.CharArrayUtil;
55 import org.jetbrains.annotations.NotNull;
56 import org.jetbrains.annotations.Nullable;
57
58 import java.awt.*;
59 import java.util.*;
60 import java.util.List;
61
62 /**
63  * Generates text with markup (in RTF and HTML formats) for interaction via clipboard with third-party applications.
64  *
65  * Interoperability with the following applications was tested:
66  *   MS Office 2010 (Word, PowerPoint, Outlook), OpenOffice (Writer, Impress), Gmail, Mac TextEdit, Mac Mail.
67  */
68 public class TextWithMarkupProcessor extends CopyPastePostProcessor<RawTextWithMarkup> {
69   private static final Logger LOG = Logger.getInstance("#" + TextWithMarkupProcessor.class.getName());
70
71   private List<RawTextWithMarkup> myResult;
72
73   @NotNull
74   @Override
75   public List<RawTextWithMarkup> collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) {
76     if (!RichCopySettings.getInstance().isEnabled()) {
77       return Collections.emptyList();
78     }
79
80     try {
81       RichCopySettings settings = RichCopySettings.getInstance();
82       List<Caret> carets = editor.getCaretModel().getAllCarets();
83       Caret firstCaret = carets.get(0);
84       final int indentSymbolsToStrip;
85       final int firstLineStartOffset;
86       if (Registry.is("editor.richcopy.strip.indents") && carets.size() == 1) {
87         Pair<Integer, Integer> p = calcIndentSymbolsToStrip(editor.getDocument(), firstCaret.getSelectionStart(), firstCaret.getSelectionEnd());
88         firstLineStartOffset = p.first;
89         indentSymbolsToStrip = p.second;
90       }
91       else {
92         firstLineStartOffset = firstCaret.getSelectionStart();
93         indentSymbolsToStrip = 0;
94       }
95       logInitial(editor, startOffsets, endOffsets, indentSymbolsToStrip, firstLineStartOffset);
96       CharSequence text = editor.getDocument().getCharsSequence();
97       EditorColorsScheme schemeToUse = settings.getColorsScheme(editor.getColorsScheme());
98       EditorHighlighter highlighter = HighlighterFactory.createHighlighter(file.getViewProvider().getVirtualFile(),
99                                                                            schemeToUse, file.getProject());
100       highlighter.setText(text);
101       MarkupModel markupModel = DocumentMarkupModel.forDocument(editor.getDocument(), file.getProject(), false);
102       Context context = new Context(text, schemeToUse, indentSymbolsToStrip);
103       int endOffset = 0;
104       Caret prevCaret = null;
105
106       for (Caret caret : carets) {
107         int caretSelectionStart = caret.getSelectionStart();
108         int caretSelectionEnd = caret.getSelectionEnd();
109         int startOffsetToUse;
110         int additionalShift = 0;
111         if (caret == firstCaret) {
112           startOffsetToUse = firstLineStartOffset;
113         }
114         else {
115           startOffsetToUse = caretSelectionStart;
116           assert prevCaret != null;
117           String prevCaretSelectedText = prevCaret.getSelectedText();
118           // Block selection fills short lines by white spaces
119           int fillStringLength = prevCaretSelectedText == null ? 0 : prevCaretSelectedText.length() - (prevCaret.getSelectionEnd() - prevCaret.getSelectionStart());
120           context.addCharacter(endOffset + fillStringLength);
121           additionalShift = fillStringLength + 1;
122         }
123         context.reset(endOffset - caretSelectionStart + additionalShift);
124         endOffset = caretSelectionEnd;
125         prevCaret = caret;
126         if (endOffset <= startOffsetToUse) {
127           continue;
128         }
129         MarkupIterator markupIterator = new MarkupIterator(text,
130                                                            new CompositeRangeIterator(schemeToUse,
131                                                                                       new HighlighterRangeIterator(highlighter, startOffsetToUse, endOffset),
132                                                                                       new MarkupModelRangeIterator(markupModel, schemeToUse, startOffsetToUse, endOffset)),
133                                                            schemeToUse);
134         try {
135           context.iterate(markupIterator, endOffset);
136         }
137         finally {
138           markupIterator.dispose();
139         }
140       }
141       SyntaxInfo syntaxInfo = context.finish();
142       logSyntaxInfo(syntaxInfo);
143
144       createResult(syntaxInfo, editor);
145       return ObjectUtils.notNull(myResult, Collections.<RawTextWithMarkup>emptyList());
146     }
147     catch (Exception e) {
148       // catching the exception so that the rest of copy/paste functionality can still work fine
149       LOG.error(e);
150     }
151     return Collections.emptyList();
152   }
153
154   @Override
155   public void processTransferableData(Project project,
156                                       Editor editor,
157                                       RangeMarker bounds,
158                                       int caretOffset,
159                                       Ref<Boolean> indented,
160                                       List<RawTextWithMarkup> values) {
161
162   }
163
164   void createResult(SyntaxInfo syntaxInfo, Editor editor) {
165     myResult = new ArrayList<RawTextWithMarkup>(2);
166     myResult.add(new HtmlTransferableData(syntaxInfo, EditorUtil.getTabSize(editor)));
167     myResult.add(new RtfTransferableData(syntaxInfo));
168   }
169
170   private void setRawText(String rawText) {
171     if (myResult == null) {
172       return;
173     }
174     for (RawTextWithMarkup data : myResult) {
175       data.setRawText(rawText);
176     }
177     myResult = null;
178   }
179
180   private static void logInitial(@NotNull Editor editor,
181                                  @NotNull int[] startOffsets,
182                                  @NotNull int[] endOffsets,
183                                  int indentSymbolsToStrip,
184                                  int firstLineStartOffset)
185   {
186     if (!LOG.isDebugEnabled()) {
187       return;
188     }
189
190     StringBuilder buffer = new StringBuilder();
191     Document document = editor.getDocument();
192     CharSequence text = document.getCharsSequence();
193     for (int i = 0; i < startOffsets.length; i++) {
194       int start = startOffsets[i];
195       int lineStart = document.getLineStartOffset(document.getLineNumber(start));
196       int end = endOffsets[i];
197       int lineEnd = document.getLineEndOffset(document.getLineNumber(end));
198       buffer.append("    region #").append(i).append(": ").append(start).append('-').append(end).append(", text at range ")
199         .append(lineStart).append('-').append(lineEnd).append(": \n'").append(text.subSequence(lineStart, lineEnd)).append("'\n");
200     }
201     if (buffer.length() > 0) {
202       buffer.setLength(buffer.length() - 1);
203     }
204     LOG.debug(String.format(
205       "Preparing syntax-aware text. Given: %s selection, indent symbols to strip=%d, first line start offset=%d, selected text:%n%s",
206       startOffsets.length > 1 ? "block" : "regular", indentSymbolsToStrip, firstLineStartOffset, buffer
207     ));
208   }
209
210   private static void logSyntaxInfo(@NotNull SyntaxInfo info) {
211     if (LOG.isDebugEnabled()) {
212       LOG.debug("Constructed syntax info: " + info);
213     }
214   }
215
216   private static Pair<Integer/* start offset to use */, Integer /* indent symbols to strip */> calcIndentSymbolsToStrip(
217     @NotNull Document document, int startOffset, int endOffset)
218   {
219     int startLine = document.getLineNumber(startOffset);
220     int endLine = document.getLineNumber(endOffset);
221     CharSequence text = document.getCharsSequence();
222     int maximumCommonIndent = Integer.MAX_VALUE;
223     int firstLineStart = startOffset;
224     int firstLineEnd = startOffset;
225     for (int line = startLine; line <= endLine; line++) {
226       int lineStartOffset = document.getLineStartOffset(line);
227       int lineEndOffset = document.getLineEndOffset(line);
228       if (line == startLine) {
229         firstLineStart = lineStartOffset;
230         firstLineEnd = lineEndOffset;
231       }
232       int nonWsOffset = lineEndOffset;
233       for (int i = lineStartOffset; i < lineEndOffset && (i - lineStartOffset) < maximumCommonIndent && i < endOffset; i++) {
234         char c = text.charAt(i);
235         if (c != ' ' && c != '\t') {
236           nonWsOffset = i;
237           break;
238         }
239       }
240       if (nonWsOffset >= lineEndOffset) {
241         continue; // Blank line
242       }
243       int indent = nonWsOffset - lineStartOffset;
244       maximumCommonIndent = Math.min(maximumCommonIndent, indent);
245       if (maximumCommonIndent == 0) {
246         break;
247       }
248     }
249     int startOffsetToUse = Math.min(firstLineEnd, Math.max(startOffset, firstLineStart + maximumCommonIndent));
250     return Pair.create(startOffsetToUse, maximumCommonIndent);
251   }
252
253   private static class Context {
254
255     private final SyntaxInfo.Builder builder;
256
257     @NotNull private final CharSequence myText;
258     @NotNull private final Color        myDefaultForeground;
259     @NotNull private final Color        myDefaultBackground;
260
261     @Nullable private Color  myBackground;
262     @Nullable private Color  myForeground;
263     @Nullable private String myFontFamilyName;
264
265     private final int myIndentSymbolsToStrip;
266
267     private int myFontStyle   = -1;
268     private int myStartOffset = -1;
269     private int myOffsetShift = 0;
270
271     private int myIndentSymbolsToStripAtCurrentLine;
272
273     Context(@NotNull CharSequence charSequence, @NotNull EditorColorsScheme scheme, int indentSymbolsToStrip) {
274       myText = charSequence;
275       myDefaultForeground = scheme.getDefaultForeground();
276       myDefaultBackground = scheme.getDefaultBackground();
277
278       // Java assumes screen resolution of 72dpi when calculating font size in pixels. External applications are supposedly using correct
279       // resolution, so we need to adjust font size for copied text to look the same in them.
280       // (See https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-Desktop/html/java2d.html#gdlwn)
281       // Java on Mac is not affected by this issue.
282       int javaFontSize = scheme.getEditorFontSize();
283       float fontSize = SystemInfo.isMac || ApplicationManager.getApplication().isHeadlessEnvironment() ? 
284                        javaFontSize : 
285                        javaFontSize * 72f / Toolkit.getDefaultToolkit().getScreenResolution();
286       
287       builder = new SyntaxInfo.Builder(myDefaultForeground, myDefaultBackground, fontSize);
288       myIndentSymbolsToStrip = indentSymbolsToStrip;
289     }
290
291     public void reset(int offsetShiftDelta) {
292       myStartOffset = -1;
293       myOffsetShift += offsetShiftDelta;
294       myIndentSymbolsToStripAtCurrentLine = 0;
295     }
296
297     public void iterate(MarkupIterator iterator, int endOffset) {
298       while (!iterator.atEnd()) {
299         iterator.advance();
300         int startOffset = iterator.getStartOffset();
301         if (startOffset >= endOffset) {
302           break;
303         }
304         if (myStartOffset < 0) {
305           myStartOffset = startOffset;
306         }
307
308         boolean whiteSpacesOnly = CharArrayUtil.isEmptyOrSpaces(myText, startOffset, iterator.getEndOffset());
309
310         processBackground(startOffset, iterator.getBackgroundColor());
311         if (!whiteSpacesOnly) {
312           processForeground(startOffset, iterator.getForegroundColor());
313           processFontFamilyName(startOffset, iterator.getFontFamilyName());
314           processFontStyle(startOffset, iterator.getFontStyle());
315         }
316       }
317       addTextIfPossible(endOffset);
318     }
319
320     private void processFontStyle(int startOffset, int fontStyle) {
321       if (fontStyle != myFontStyle) {
322         addTextIfPossible(startOffset);
323         builder.addFontStyle(fontStyle);
324         myFontStyle = fontStyle;
325       }
326     }
327
328     private void processFontFamilyName(int startOffset, String fontName) {
329       String fontFamilyName = FontMapper.getPhysicalFontName(fontName);
330       if (!fontFamilyName.equals(myFontFamilyName)) {
331         addTextIfPossible(startOffset);
332         builder.addFontFamilyName(fontFamilyName);
333         myFontFamilyName = fontFamilyName;
334       }
335     }
336
337     private void processForeground(int startOffset, Color foreground) {
338       if (myForeground == null && foreground != null) {
339         addTextIfPossible(startOffset);
340         myForeground = foreground;
341         builder.addForeground(foreground);
342       }
343       else if (myForeground != null) {
344         Color c = foreground == null ? myDefaultForeground : foreground;
345         if (!myForeground.equals(c)) {
346           addTextIfPossible(startOffset);
347           builder.addForeground(c);
348           myForeground = c;
349         }
350       }
351     }
352
353     private void processBackground(int startOffset, Color background) {
354       if (myBackground == null && background != null && !myDefaultBackground.equals(background)) {
355         addTextIfPossible(startOffset);
356         myBackground = background;
357         builder.addBackground(background);
358       }
359       else if (myBackground != null) {
360         Color c = background == null ? myDefaultBackground : background;
361         if (!myBackground.equals(c)) {
362           addTextIfPossible(startOffset);
363           builder.addBackground(c);
364           myBackground = c;
365         }
366       }
367     }
368
369     private void addTextIfPossible(int endOffset) {
370       if (endOffset <= myStartOffset) {
371         return;
372       }
373
374       for (int i = myStartOffset; i < endOffset; i++) {
375         char c = myText.charAt(i);
376         switch (c) {
377           case '\r':
378             if (i + 1 < myText.length() && myText.charAt(i + 1) == '\n') {
379               myIndentSymbolsToStripAtCurrentLine = myIndentSymbolsToStrip;
380               builder.addText(myStartOffset + myOffsetShift, i + myOffsetShift + 1);
381               myStartOffset = i + 2;
382               myOffsetShift--;
383               //noinspection AssignmentToForLoopParameter
384               i++;
385               break;
386             }
387             // Intended fall-through.
388           case '\n':
389             myIndentSymbolsToStripAtCurrentLine = myIndentSymbolsToStrip;
390             builder.addText(myStartOffset + myOffsetShift, i + myOffsetShift + 1);
391             myStartOffset = i + 1;
392             break;
393           // Intended fall-through.
394           case ' ':
395           case '\t':
396             if (myIndentSymbolsToStripAtCurrentLine > 0) {
397               myIndentSymbolsToStripAtCurrentLine--;
398               myStartOffset++;
399               continue;
400             }
401           default: myIndentSymbolsToStripAtCurrentLine = 0;
402         }
403       }
404
405       if (myStartOffset < endOffset) {
406         builder.addText(myStartOffset + myOffsetShift, endOffset + myOffsetShift);
407         myStartOffset = endOffset;
408       }
409     }
410
411     private void addCharacter(int position) {
412       builder.addText(position + myOffsetShift, position + myOffsetShift + 1);
413     }
414
415     @NotNull
416     public SyntaxInfo finish() {
417       return builder.build();
418     }
419   }
420
421   private static class MarkupIterator {
422     private final SegmentIterator mySegmentIterator;
423     private final RangeIterator myRangeIterator;
424     private int myCurrentFontStyle;
425     private Color myCurrentForegroundColor;
426     private Color myCurrentBackgroundColor;
427
428     private MarkupIterator(@NotNull CharSequence charSequence, @NotNull RangeIterator rangeIterator, @NotNull EditorColorsScheme colorsScheme) {
429       myRangeIterator = rangeIterator;
430       mySegmentIterator = new SegmentIterator(charSequence, colorsScheme.getFontPreferences());
431     }
432
433     public boolean atEnd() {
434       return myRangeIterator.atEnd() && mySegmentIterator.atEnd();
435     }
436
437     public void advance() {
438       if (mySegmentIterator.atEnd()) {
439         myRangeIterator.advance();
440         TextAttributes textAttributes = myRangeIterator.getTextAttributes();
441         myCurrentFontStyle = textAttributes == null ? Font.PLAIN : textAttributes.getFontType();
442         myCurrentForegroundColor = textAttributes == null ? null : textAttributes.getForegroundColor();
443         myCurrentBackgroundColor = textAttributes == null ? null : textAttributes.getBackgroundColor();
444         mySegmentIterator.reset(myRangeIterator.getRangeStart(), myRangeIterator.getRangeEnd(), myCurrentFontStyle);
445       }
446       mySegmentIterator.advance();
447     }
448
449     public int getStartOffset() {
450       return mySegmentIterator.getCurrentStartOffset();
451     }
452
453     public int getEndOffset() {
454       return mySegmentIterator.getCurrentEndOffset();
455     }
456
457     public int getFontStyle() {
458       return myCurrentFontStyle;
459     }
460
461     @NotNull
462     public String getFontFamilyName() {
463       return mySegmentIterator.getCurrentFontFamilyName();
464     }
465
466     @Nullable
467     public Color getForegroundColor() {
468       return myCurrentForegroundColor;
469     }
470
471     @Nullable
472     public Color getBackgroundColor() {
473       return myCurrentBackgroundColor;
474     }
475
476     public void dispose() {
477       myRangeIterator.dispose();
478     }
479   }
480
481   private static class CompositeRangeIterator implements RangeIterator {
482     private final @NotNull Color myDefaultForeground;
483     private final @NotNull Color myDefaultBackground;
484     private final IteratorWrapper[] myIterators;
485     private final TextAttributes myMergedAttributes = new TextAttributes();
486     private int overlappingRangesCount;
487     private int myCurrentStart;
488     private int myCurrentEnd;
489
490     // iterators have priority corresponding to their order in the parameter list - rightmost having the largest priority
491     public CompositeRangeIterator(@NotNull EditorColorsScheme colorsScheme, RangeIterator... iterators) {
492       myDefaultForeground = colorsScheme.getDefaultForeground();
493       myDefaultBackground = colorsScheme.getDefaultBackground();
494       myIterators = new IteratorWrapper[iterators.length];
495       for (int i = 0; i < iterators.length; i++) {
496         myIterators[i] = new IteratorWrapper(iterators[i], i);
497       }
498     }
499
500     @Override
501     public boolean atEnd() {
502       boolean validIteratorExists = false;
503       for (int i = 0; i < myIterators.length; i++) {
504         IteratorWrapper wrapper = myIterators[i];
505         if (wrapper == null) {
506           continue;
507         }
508         RangeIterator iterator = wrapper.iterator;
509         if (!iterator.atEnd() || overlappingRangesCount > 0 && (i >= overlappingRangesCount || iterator.getRangeEnd() > myCurrentEnd)) {
510           validIteratorExists = true;
511         }
512       }
513       return !validIteratorExists;
514     }
515
516     @Override
517     public void advance() {
518       int max = overlappingRangesCount == 0 ? myIterators.length : overlappingRangesCount;
519       for (int i = 0; i < max; i++) {
520         IteratorWrapper wrapper = myIterators[i];
521         if (wrapper == null) {
522           continue;
523         }
524         RangeIterator iterator = wrapper.iterator;
525         if (overlappingRangesCount > 0 && iterator.getRangeEnd() > myCurrentEnd) {
526           continue;
527         }
528         if (iterator.atEnd()) {
529           iterator.dispose();
530           myIterators[i] = null;
531         }
532         else {
533           iterator.advance();
534         }
535       }
536       Arrays.sort(myIterators, RANGE_SORTER);
537       myCurrentStart = Math.max(myIterators[0].iterator.getRangeStart(), myCurrentEnd);
538       myCurrentEnd = Integer.MAX_VALUE;
539       //noinspection ForLoopReplaceableByForEach
540       for (int i = 0; i < myIterators.length; i++) {
541         IteratorWrapper wrapper = myIterators[i];
542         if (wrapper == null) {
543           break;
544         }
545         RangeIterator iterator = wrapper.iterator;
546         int nearestBound;
547         if (iterator.getRangeStart() > myCurrentStart) {
548           nearestBound = iterator.getRangeStart();
549         }
550         else {
551           nearestBound = iterator.getRangeEnd();
552         }
553         myCurrentEnd = Math.min(myCurrentEnd, nearestBound);
554       }
555       for (overlappingRangesCount = 1; overlappingRangesCount < myIterators.length; overlappingRangesCount++) {
556         IteratorWrapper wrapper = myIterators[overlappingRangesCount];
557         if (wrapper == null || wrapper.iterator.getRangeStart() > myCurrentStart) {
558           break;
559         }
560       }
561     }
562
563     private final Comparator<IteratorWrapper> RANGE_SORTER  = new Comparator<IteratorWrapper>() {
564       @Override
565       public int compare(IteratorWrapper o1, IteratorWrapper o2) {
566         if (o1 == null) {
567           return 1;
568         }
569         if (o2 == null) {
570           return -1;
571         }
572         int startDiff = Math.max(o1.iterator.getRangeStart(), myCurrentEnd) - Math.max(o2.iterator.getRangeStart(), myCurrentEnd);
573         if (startDiff != 0) {
574           return startDiff;
575         }
576         return o2.order - o1.order;
577       }
578     };
579
580     @Override
581     public int getRangeStart() {
582       return myCurrentStart;
583     }
584
585     @Override
586     public int getRangeEnd() {
587       return myCurrentEnd;
588     }
589
590     @Override
591     public TextAttributes getTextAttributes() {
592       TextAttributes ta = myIterators[0].iterator.getTextAttributes();
593       myMergedAttributes.setAttributes(ta.getForegroundColor(), ta.getBackgroundColor(), null, null, null, ta.getFontType());
594       for (int i = 1; i < overlappingRangesCount; i++) {
595         merge(myIterators[i].iterator.getTextAttributes());
596       }
597       return myMergedAttributes;
598     }
599
600     private void merge(TextAttributes attributes) {
601       Color myBackground = myMergedAttributes.getBackgroundColor();
602       if (myBackground == null || myDefaultBackground.equals(myBackground)) {
603         myMergedAttributes.setBackgroundColor(attributes.getBackgroundColor());
604       }
605       Color myForeground = myMergedAttributes.getForegroundColor();
606       if (myForeground == null || myDefaultForeground.equals(myForeground)) {
607         myMergedAttributes.setForegroundColor(attributes.getForegroundColor());
608       }
609       if (myMergedAttributes.getFontType() == Font.PLAIN) {
610         myMergedAttributes.setFontType(attributes.getFontType());
611       }
612     }
613
614     @Override
615     public void dispose() {
616       for (IteratorWrapper wrapper : myIterators) {
617         if (wrapper != null) {
618           wrapper.iterator.dispose();
619         }
620       }
621     }
622
623     private static class IteratorWrapper {
624       private final RangeIterator iterator;
625       private final int order;
626
627       private IteratorWrapper(RangeIterator iterator, int order) {
628         this.iterator = iterator;
629         this.order = order;
630       }
631     }
632   }
633
634   private static class MarkupModelRangeIterator implements RangeIterator {
635     private final boolean myUnsupportedModel;
636     private final int myStartOffset;
637     private final int myEndOffset;
638     private final EditorColorsScheme myColorsScheme;
639     private final Color myDefaultForeground;
640     private final Color myDefaultBackground;
641     private final DisposableIterator<RangeHighlighterEx> myIterator;
642
643     private int myCurrentStart;
644     private int myCurrentEnd;
645     private TextAttributes myCurrentAttributes;
646     private int myNextStart;
647     private int myNextEnd;
648     private TextAttributes myNextAttributes;
649
650     private MarkupModelRangeIterator(@Nullable MarkupModel markupModel,
651                                      @NotNull EditorColorsScheme colorsScheme,
652                                      int startOffset,
653                                      int endOffset) {
654       myStartOffset = startOffset;
655       myEndOffset = endOffset;
656       myColorsScheme = colorsScheme;
657       myDefaultForeground = colorsScheme.getDefaultForeground();
658       myDefaultBackground = colorsScheme.getDefaultBackground();
659       myUnsupportedModel = !(markupModel instanceof MarkupModelEx);
660       if (myUnsupportedModel) {
661         myIterator = null;
662         return;
663       }
664       myIterator = ((MarkupModelEx)markupModel).overlappingIterator(startOffset, endOffset);
665       try {
666         findNextSuitableRange();
667       }
668       catch (RuntimeException e) {
669         myIterator.dispose();
670         throw e;
671       }
672       catch (Error e) {
673         myIterator.dispose();
674         throw e;
675       }
676     }
677
678     @Override
679     public boolean atEnd() {
680       return myUnsupportedModel || myNextAttributes == null;
681     }
682
683     @Override
684     public void advance() {
685       myCurrentStart = myNextStart;
686       myCurrentEnd = myNextEnd;
687       myCurrentAttributes = myNextAttributes;
688       findNextSuitableRange();
689     }
690
691     private void findNextSuitableRange() {
692       myNextAttributes = null;
693       while(myIterator.hasNext()) {
694         RangeHighlighterEx highlighter = myIterator.next();
695         if (highlighter == null || !highlighter.isValid() || !isInterestedInLayer(highlighter.getLayer())) {
696           continue;
697         }
698         // LINES_IN_RANGE highlighters are not supported currently
699         myNextStart = Math.max(highlighter.getStartOffset(), myStartOffset);
700         myNextEnd = Math.min(highlighter.getEndOffset(), myEndOffset);
701         if (myNextStart >= myEndOffset) {
702           break;
703         }
704         if (myNextStart < myCurrentEnd) {
705           continue; // overlapping ranges withing document markup model are not supported currently
706         }
707         TextAttributes attributes = null;
708         Object tooltip = highlighter.getErrorStripeTooltip();
709         if (tooltip instanceof HighlightInfo) {
710           HighlightInfo info = (HighlightInfo)tooltip;
711           TextAttributesKey key = info.forcedTextAttributesKey;
712           if (key == null) {
713             HighlightInfoType type = info.type;
714             key = type.getAttributesKey();
715           }
716           if (key != null) {
717             attributes = myColorsScheme.getAttributes(key);
718           }
719         }
720         if (attributes == null) {
721           continue;
722         }
723         Color foreground = attributes.getForegroundColor();
724         Color background = attributes.getBackgroundColor();
725         if ((foreground == null || myDefaultForeground.equals(foreground))
726             && (background == null || myDefaultBackground.equals(background))
727             && attributes.getFontType() == Font.PLAIN) {
728           continue;
729         }
730         myNextAttributes = attributes;
731         break;
732       }
733     }
734
735     private static boolean isInterestedInLayer(int layer) {
736       return layer != HighlighterLayer.CARET_ROW
737              && layer != HighlighterLayer.SELECTION
738              && layer != HighlighterLayer.ERROR
739              && layer != HighlighterLayer.WARNING;
740     }
741
742     @Override
743     public int getRangeStart() {
744       return myCurrentStart;
745     }
746
747     @Override
748     public int getRangeEnd() {
749       return myCurrentEnd;
750     }
751
752     @Override
753     public TextAttributes getTextAttributes() {
754       return myCurrentAttributes;
755     }
756
757     @Override
758     public void dispose() {
759       if (myIterator != null) {
760         myIterator.dispose();
761       }
762     }
763   }
764
765   private static class HighlighterRangeIterator implements RangeIterator {
766     private static final TextAttributes EMPTY_ATTRIBUTES = new TextAttributes();
767
768     private final HighlighterIterator myIterator;
769     private final int myStartOffset;
770     private final int myEndOffset;
771
772     private int myCurrentStart;
773     private int myCurrentEnd;
774     private TextAttributes myCurrentAttributes;
775
776     public HighlighterRangeIterator(@NotNull EditorHighlighter highlighter, int startOffset, int endOffset) {
777       myStartOffset = startOffset;
778       myEndOffset = endOffset;
779       myIterator = highlighter.createIterator(startOffset);
780     }
781
782     @Override
783     public boolean atEnd() {
784       return myIterator.atEnd() || getCurrentStart() >= myEndOffset;
785     }
786
787     private int getCurrentStart() {
788       return Math.max(myIterator.getStart(), myStartOffset);
789     }
790
791     private int getCurrentEnd() {
792       return Math.min(myIterator.getEnd(), myEndOffset);
793     }
794
795     @Override
796     public void advance() {
797       myCurrentStart = getCurrentStart();
798       myCurrentEnd = getCurrentEnd();
799       myCurrentAttributes = myIterator.getTokenType() == TokenType.BAD_CHARACTER ? EMPTY_ATTRIBUTES : myIterator.getTextAttributes();
800       myIterator.advance();
801     }
802
803     @Override
804     public int getRangeStart() {
805       return myCurrentStart;
806     }
807
808     @Override
809     public int getRangeEnd() {
810       return myCurrentEnd;
811     }
812
813     @Override
814     public TextAttributes getTextAttributes() {
815       return myCurrentAttributes;
816     }
817
818     @Override
819     public void dispose() {
820     }
821   }
822
823   private interface RangeIterator {
824     boolean atEnd();
825     void advance();
826     int getRangeStart();
827     int getRangeEnd();
828     TextAttributes getTextAttributes();
829     void dispose();
830   }
831
832   private static class SegmentIterator {
833     private final CharSequence myCharSequence;
834     private final FontPreferences myFontPreferences;
835
836     private int myCurrentStartOffset;
837     private int myCurrentOffset;
838     private int myEndOffset;
839     private int myFontStyle;
840     private String myCurrentFontFamilyName;
841     private String myNextFontFamilyName;
842
843     private SegmentIterator(CharSequence charSequence, FontPreferences fontPreferences) {
844       myCharSequence = charSequence;
845       myFontPreferences = fontPreferences;
846     }
847
848     public void reset(int startOffset, int endOffset, int fontStyle) {
849       myCurrentOffset = startOffset;
850       myEndOffset = endOffset;
851       myFontStyle = fontStyle;
852     }
853
854     public boolean atEnd() {
855       return myCurrentOffset >= myEndOffset;
856     }
857
858     public void advance() {
859       myCurrentFontFamilyName = myNextFontFamilyName;
860       myCurrentStartOffset = myCurrentOffset;
861       for (; myCurrentOffset < myEndOffset; myCurrentOffset++) {
862         FontInfo fontInfo = ComplementaryFontsRegistry.getFontAbleToDisplay(myCharSequence.charAt(myCurrentOffset),
863                                                                             myFontStyle,
864                                                                             myFontPreferences);
865         String fontFamilyName = fontInfo.getFont().getFamily();
866
867         if (myCurrentFontFamilyName == null) {
868           myCurrentFontFamilyName = fontFamilyName;
869         }
870         else if (!myCurrentFontFamilyName.equals(fontFamilyName)) {
871           myNextFontFamilyName = fontFamilyName;
872           break;
873         }
874       }
875     }
876
877     public int getCurrentStartOffset() {
878       return myCurrentStartOffset;
879     }
880
881     public int getCurrentEndOffset() {
882       return myCurrentOffset;
883     }
884
885     public String getCurrentFontFamilyName() {
886       return myCurrentFontFamilyName;
887     }
888   }
889
890   public static class RawTextSetter implements CopyPastePreProcessor {
891     private final TextWithMarkupProcessor myProcessor;
892
893     public RawTextSetter(TextWithMarkupProcessor processor) {
894       myProcessor = processor;
895     }
896
897     @Nullable
898     @Override
899     public String preprocessOnCopy(PsiFile file, int[] startOffsets, int[] endOffsets, String text) {
900       myProcessor.setRawText(text);
901       return null;
902     }
903
904     @NotNull
905     @Override
906     public String preprocessOnPaste(Project project, PsiFile file, Editor editor, String text, RawText rawText) {
907       return text;
908     }
909   }
910 }