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