delete some deprecated stuff
[idea/community.git] / platform / testFramework / src / com / intellij / testFramework / ExpectedHighlightingData.java
1 /*
2  * Copyright 2000-2015 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.testFramework;
17
18 import com.intellij.codeHighlighting.Pass;
19 import com.intellij.codeInsight.daemon.LineMarkerInfo;
20 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
21 import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
22 import com.intellij.codeInsight.daemon.impl.SeveritiesProvider;
23 import com.intellij.lang.annotation.HighlightSeverity;
24 import com.intellij.openapi.application.Result;
25 import com.intellij.openapi.command.WriteCommandAction;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.HighlighterColors;
29 import com.intellij.openapi.editor.RangeMarker;
30 import com.intellij.openapi.editor.colors.TextAttributesKey;
31 import com.intellij.openapi.editor.markup.EffectType;
32 import com.intellij.openapi.editor.markup.GutterIconRenderer;
33 import com.intellij.openapi.editor.markup.TextAttributes;
34 import com.intellij.openapi.extensions.Extensions;
35 import com.intellij.openapi.util.*;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.psi.PsiElement;
39 import com.intellij.psi.PsiFile;
40 import com.intellij.rt.execution.junit.FileComparisonFailure;
41 import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
42 import com.intellij.util.ConstantFunction;
43 import com.intellij.util.NullableFunction;
44 import com.intellij.util.containers.ContainerUtil;
45 import gnu.trove.THashMap;
46 import gnu.trove.THashSet;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49 import org.junit.Assert;
50
51 import java.awt.*;
52 import java.lang.reflect.Field;
53 import java.util.*;
54 import java.util.List;
55 import java.util.regex.Matcher;
56 import java.util.regex.Pattern;
57
58 /**
59  * @author cdr
60  */
61 public class ExpectedHighlightingData {
62   private static final Logger LOG = Logger.getInstance("#com.intellij.testFramework.ExpectedHighlightingData");
63
64   private static final String ERROR_MARKER = CodeInsightTestFixture.ERROR_MARKER;
65   private static final String WARNING_MARKER = CodeInsightTestFixture.WARNING_MARKER;
66   private static final String WEAK_WARNING_MARKER = CodeInsightTestFixture.WEAK_WARNING_MARKER;
67   private static final String INFO_MARKER = CodeInsightTestFixture.INFO_MARKER;
68   private static final String END_LINE_HIGHLIGHT_MARKER = CodeInsightTestFixture.END_LINE_HIGHLIGHT_MARKER;
69   private static final String END_LINE_WARNING_MARKER = CodeInsightTestFixture.END_LINE_WARNING_MARKER;
70   private static final String INJECT_MARKER = "inject";
71   private static final String SYMBOL_NAME_MARKER = "symbolName";
72   private static final String LINE_MARKER = "lineMarker";
73   private static final String ANY_TEXT = "*";
74
75   public static class ExpectedHighlightingSet {
76     private final HighlightSeverity severity;
77     private final boolean endOfLine;
78     private final boolean enabled;
79     private final Set<HighlightInfo> infos;
80
81     public ExpectedHighlightingSet(@NotNull HighlightSeverity severity, boolean endOfLine, boolean enabled) {
82       this.severity = severity;
83       this.endOfLine = endOfLine;
84       this.enabled = enabled;
85       this.infos = new THashSet<>();
86     }
87   }
88
89   private final Map<String, ExpectedHighlightingSet> myHighlightingTypes = new LinkedHashMap<>();
90   private final Map<RangeMarker, LineMarkerInfo> myLineMarkerInfos = new THashMap<>();
91   private final Document myDocument;
92   @SuppressWarnings("StatefulEp") private final PsiFile myFile;
93   private final String myText;
94   private boolean myIgnoreExtraHighlighting;
95
96   public ExpectedHighlightingData(@NotNull Document document,boolean checkWarnings, boolean checkInfos) {
97     this(document, checkWarnings, false, checkInfos);
98   }
99
100   public ExpectedHighlightingData(@NotNull Document document, boolean checkWarnings, boolean checkWeakWarnings, boolean checkInfos) {
101     this(document, checkWarnings, checkWeakWarnings, checkInfos, null);
102   }
103
104   public ExpectedHighlightingData(@NotNull Document document,
105                                   boolean checkWarnings,
106                                   boolean checkWeakWarnings,
107                                   boolean checkInfos,
108                                   @Nullable PsiFile file) {
109     this(document, checkWarnings, checkWeakWarnings, checkInfos, false, file);
110   }
111
112   public ExpectedHighlightingData(@NotNull Document document,
113                                   boolean checkWarnings,
114                                   boolean checkWeakWarnings,
115                                   boolean checkInfos,
116                                   boolean ignoreExtraHighlighting,
117                                   @Nullable PsiFile file) {
118     this(document, file);
119     myIgnoreExtraHighlighting = ignoreExtraHighlighting;
120     if (checkWarnings) checkWarnings();
121     if (checkWeakWarnings) checkWeakWarnings();
122     if (checkInfos) checkInfos();
123   }
124
125   @SuppressWarnings("deprecation")
126   public ExpectedHighlightingData(@NotNull Document document, @Nullable PsiFile file) {
127     myDocument = document;
128     myFile = file;
129     myText = document.getText();
130
131     registerHighlightingType(ERROR_MARKER, new ExpectedHighlightingSet(HighlightSeverity.ERROR, false, true));
132     registerHighlightingType(WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WARNING, false, false));
133     registerHighlightingType(WEAK_WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WEAK_WARNING, false, false));
134     registerHighlightingType(INJECT_MARKER, new ExpectedHighlightingSet(HighlightInfoType.INJECTED_FRAGMENT_SEVERITY, false, false));
135     registerHighlightingType(INFO_MARKER, new ExpectedHighlightingSet(HighlightSeverity.INFORMATION, false, false));
136     registerHighlightingType(SYMBOL_NAME_MARKER, new ExpectedHighlightingSet(HighlightInfoType.SYMBOL_TYPE_SEVERITY, false, false));
137     for (SeveritiesProvider provider : Extensions.getExtensions(SeveritiesProvider.EP_NAME)) {
138       for (HighlightInfoType type : provider.getSeveritiesHighlightInfoTypes()) {
139         HighlightSeverity severity = type.getSeverity(null);
140         registerHighlightingType(severity.getName(), new ExpectedHighlightingSet(severity, false, true));
141       }
142     }
143     registerHighlightingType(END_LINE_HIGHLIGHT_MARKER, new ExpectedHighlightingSet(HighlightSeverity.ERROR, true, true));
144     registerHighlightingType(END_LINE_WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WARNING, true, false));
145   }
146
147   public void init() {
148     new WriteCommandAction(null) {
149       @Override
150       protected void run(@NotNull Result result) {
151         extractExpectedLineMarkerSet(myDocument);
152         extractExpectedHighlightsSet(myDocument);
153         refreshLineMarkers();
154       }
155     }.execute();
156   }
157
158   public void checkWarnings() {
159     registerHighlightingType(WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WARNING, false, true));
160     registerHighlightingType(END_LINE_WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WARNING, true, true));
161   }
162
163   public void checkWeakWarnings() {
164     registerHighlightingType(WEAK_WARNING_MARKER, new ExpectedHighlightingSet(HighlightSeverity.WEAK_WARNING, false, true));
165   }
166
167   public void checkInfos() {
168     registerHighlightingType(INFO_MARKER, new ExpectedHighlightingSet(HighlightSeverity.INFORMATION, false, true));
169     registerHighlightingType(INJECT_MARKER, new ExpectedHighlightingSet(HighlightInfoType.INJECTED_FRAGMENT_SEVERITY, false, true));
170   }
171
172   public void checkSymbolNames() {
173     registerHighlightingType(SYMBOL_NAME_MARKER, new ExpectedHighlightingSet(HighlightInfoType.SYMBOL_TYPE_SEVERITY, false, true));
174   }
175
176   public void registerHighlightingType(@NotNull String key, @NotNull ExpectedHighlightingSet highlightingSet) {
177     myHighlightingTypes.put(key, highlightingSet);
178   }
179
180   private void refreshLineMarkers() {
181     for (Map.Entry<RangeMarker, LineMarkerInfo> entry : myLineMarkerInfos.entrySet()) {
182       RangeMarker rangeMarker = entry.getKey();
183       int startOffset = rangeMarker.getStartOffset();
184       int endOffset = rangeMarker.getEndOffset();
185       LineMarkerInfo value = entry.getValue();
186       PsiElement element = value.getElement();
187       assert element != null : value;
188       TextRange range = new TextRange(startOffset, endOffset);
189       final String tooltip = value.getLineMarkerTooltip();
190       LineMarkerInfo markerInfo =
191         new LineMarkerInfo<>(element, range, null, value.updatePass, e -> tooltip, null, GutterIconRenderer.Alignment.RIGHT);
192       entry.setValue(markerInfo);
193     }
194   }
195
196   private void extractExpectedLineMarkerSet(Document document) {
197     String text = document.getText();
198
199     String pat = ".*?((<" + LINE_MARKER + ")(?: descr=\"((?:[^\"\\\\]|\\\\\")*)\")?>)(.*)";
200     final Pattern p = Pattern.compile(pat, Pattern.DOTALL);
201     final Pattern pat2 = Pattern.compile("(.*?)(</" + LINE_MARKER + ">)(.*)", Pattern.DOTALL);
202
203     while (true) {
204       Matcher m = p.matcher(text);
205       if (!m.matches()) break;
206       int startOffset = m.start(1);
207       final String descr = m.group(3) != null ? m.group(3) : ANY_TEXT;
208       String rest = m.group(4);
209
210       document.replaceString(startOffset, m.end(1), "");
211
212       final Matcher matcher2 = pat2.matcher(rest);
213       LOG.assertTrue(matcher2.matches(), "Cannot find closing </" + LINE_MARKER + ">");
214       String content = matcher2.group(1);
215       int endOffset = startOffset + matcher2.start(3);
216       String endTag = matcher2.group(2);
217
218       document.replaceString(startOffset, endOffset, content);
219       endOffset -= endTag.length();
220
221       LineMarkerInfo markerInfo = new LineMarkerInfo<PsiElement>(myFile, new TextRange(startOffset, endOffset), null, Pass.LINE_MARKERS,
222                                                                  new ConstantFunction<>(descr), null,
223                                                                  GutterIconRenderer.Alignment.RIGHT);
224
225       myLineMarkerInfos.put(document.createRangeMarker(startOffset, endOffset), markerInfo);
226       text = document.getText();
227     }
228   }
229
230   /**
231    * remove highlights (bounded with <marker>...</marker>) from test case file
232    * @param document document to process
233    */
234   private void extractExpectedHighlightsSet(final Document document) {
235     final String text = document.getText();
236
237     final Set<String> markers = myHighlightingTypes.keySet();
238     final String typesRx = "(?:" + StringUtil.join(markers, ")|(?:") + ")";
239     final String openingTagRx = "<(" + typesRx + ")" +
240                                 "(?:\\s+descr=\"((?:[^\"]|\\\\\"|\\\\\\\\\"|\\\\\\[|\\\\\\])*)\")?" +
241                                 "(?:\\s+type=\"([0-9A-Z_]+)\")?" +
242                                 "(?:\\s+foreground=\"([0-9xa-f]+)\")?" +
243                                 "(?:\\s+background=\"([0-9xa-f]+)\")?" +
244                                 "(?:\\s+effectcolor=\"([0-9xa-f]+)\")?" +
245                                 "(?:\\s+effecttype=\"([A-Z]+)\")?" +
246                                 "(?:\\s+fonttype=\"([0-9]+)\")?" +
247                                 "(?:\\s+textAttributesKey=\"((?:[^\"]|\\\\\"|\\\\\\\\\"|\\\\\\[|\\\\\\])*)\")?" +
248                                 "(/)?>";
249
250     final Matcher matcher = Pattern.compile(openingTagRx).matcher(text);
251     int pos = 0;
252     final Ref<Integer> textOffset = Ref.create(0);
253     while (matcher.find(pos)) {
254       textOffset.set(textOffset.get() + matcher.start() - pos);
255       pos = extractExpectedHighlight(matcher, text, document, textOffset);
256     }
257   }
258
259   private int extractExpectedHighlight(final Matcher matcher, final String text, final Document document, final Ref<Integer> textOffset) {
260     document.deleteString(textOffset.get(), textOffset.get() + matcher.end() - matcher.start());
261
262     int groupIdx = 1;
263     final String marker = matcher.group(groupIdx++);
264     String descr = matcher.group(groupIdx++);
265     final String typeString = matcher.group(groupIdx++);
266     final String foregroundColor = matcher.group(groupIdx++);
267     final String backgroundColor = matcher.group(groupIdx++);
268     final String effectColor = matcher.group(groupIdx++);
269     final String effectType = matcher.group(groupIdx++);
270     final String fontType = matcher.group(groupIdx++);
271     final String attrKey = matcher.group(groupIdx++);
272     final boolean closed = matcher.group(groupIdx) != null;
273
274     if (descr == null) {
275       descr = ANY_TEXT;  // no descr means any string by default
276     }
277     else if (descr.equals("null")) {
278       descr = null;  // explicit "null" descr
279     }
280     if (descr != null) {
281       descr = descr.replaceAll("\\\\\\\\\"", "\"");  // replace: \\" to ", doesn't check symbol before sequence \\"
282       descr = descr.replaceAll("\\\\\"", "\"");
283     }
284
285     HighlightInfoType type = WHATEVER;
286     if (typeString != null) {
287       try {
288         type = getTypeByName(typeString);
289       }
290       catch (Exception e) {
291         LOG.error(e);
292       }
293       LOG.assertTrue(type != null, "Wrong highlight type: " + typeString);
294     }
295
296     TextAttributes forcedAttributes = null;
297     if (foregroundColor != null) {
298       //noinspection MagicConstant
299       forcedAttributes = new TextAttributes(Color.decode(foregroundColor), Color.decode(backgroundColor), Color.decode(effectColor),
300                                             EffectType.valueOf(effectType), Integer.parseInt(fontType));
301     }
302
303     final int rangeStart = textOffset.get();
304     final int toContinueFrom;
305     if (closed) {
306       toContinueFrom = matcher.end();
307     }
308     else {
309       int pos = matcher.end();
310       final Matcher closingTagMatcher = Pattern.compile("</" + marker + ">").matcher(text);
311       while (true) {
312         if (!closingTagMatcher.find(pos)) {
313           LOG.error("Cannot find closing </" + marker + "> in position " + pos);
314         }
315
316         final int nextTagStart = matcher.find(pos) ? matcher.start() : text.length();
317         if (closingTagMatcher.start() < nextTagStart) {
318           textOffset.set(textOffset.get() + closingTagMatcher.start() - pos);
319           document.deleteString(textOffset.get(), textOffset.get() + closingTagMatcher.end() - closingTagMatcher.start());
320           toContinueFrom = closingTagMatcher.end();
321           break;
322         }
323
324         textOffset.set(textOffset.get() + nextTagStart - pos);
325         pos = extractExpectedHighlight(matcher, text, document, textOffset);
326       }
327     }
328
329     final ExpectedHighlightingSet expectedHighlightingSet = myHighlightingTypes.get(marker);
330     if (expectedHighlightingSet.enabled) {
331       TextAttributesKey forcedTextAttributesKey = attrKey == null ? null : TextAttributesKey.createTextAttributesKey(attrKey);
332       HighlightInfo.Builder builder =
333         HighlightInfo.newHighlightInfo(type).range(rangeStart, textOffset.get()).severity(expectedHighlightingSet.severity);
334
335       if (forcedAttributes != null) builder.textAttributes(forcedAttributes);
336       if (forcedTextAttributesKey != null) builder.textAttributes(forcedTextAttributesKey);
337       if (descr != null) { builder.description(descr); builder.unescapedToolTip(descr); }
338       if (expectedHighlightingSet.endOfLine) builder.endOfLine();
339       HighlightInfo highlightInfo = builder.createUnconditionally();
340       expectedHighlightingSet.infos.add(highlightInfo);
341     }
342
343     return toContinueFrom;
344   }
345
346   protected HighlightInfoType getTypeByName(String typeString) throws Exception {
347     Field field = HighlightInfoType.class.getField(typeString);
348     return  (HighlightInfoType)field.get(null);
349   }
350
351   private static final HighlightInfoType WHATEVER = new HighlightInfoType.HighlightInfoTypeImpl(HighlightSeverity.INFORMATION,
352                                                                                                 HighlighterColors.TEXT);
353
354   public void checkLineMarkers(@NotNull Collection<LineMarkerInfo> markerInfos, @NotNull String text) {
355     String fileName = myFile == null ? "" : myFile.getName() + ": ";
356     String failMessage = "";
357
358     for (LineMarkerInfo info : markerInfos) {
359       if (!containsLineMarker(info, myLineMarkerInfos.values())) {
360         if (!failMessage.isEmpty()) failMessage += '\n';
361         failMessage += fileName + "Extra line marker highlighted " +
362                           rangeString(text, info.startOffset, info.endOffset)
363                           + ": '"+info.getLineMarkerTooltip()+"'"
364                           ;
365       }
366     }
367
368     for (LineMarkerInfo expectedLineMarker : myLineMarkerInfos.values()) {
369       if (!markerInfos.isEmpty() && !containsLineMarker(expectedLineMarker, markerInfos)) {
370         if (!failMessage.isEmpty()) failMessage += '\n';
371         failMessage += fileName + "Line marker was not highlighted " +
372                        rangeString(text, expectedLineMarker.startOffset, expectedLineMarker.endOffset)
373                        + ": '"+expectedLineMarker.getLineMarkerTooltip()+"'"
374           ;
375       }
376     }
377
378     if (!failMessage.isEmpty()) Assert.fail(failMessage);
379   }
380
381   private static boolean containsLineMarker(LineMarkerInfo info, Collection<LineMarkerInfo> where) {
382     final String infoTooltip = info.getLineMarkerTooltip();
383
384     for (LineMarkerInfo markerInfo : where) {
385       String markerInfoTooltip;
386       if (markerInfo.startOffset == info.startOffset &&
387           markerInfo.endOffset == info.endOffset &&
388           ( Comparing.equal(infoTooltip, markerInfoTooltip = markerInfo.getLineMarkerTooltip())  ||
389             ANY_TEXT.equals(markerInfoTooltip) ||
390             ANY_TEXT.equals(infoTooltip)
391           )
392         ) {
393         return true;
394       }
395     }
396     return false;
397   }
398
399   public void checkResult(Collection<HighlightInfo> infos, String text) {
400     checkResult(infos, text, null);
401   }
402
403   public void checkResult(Collection<HighlightInfo> infos, String text, @Nullable String filePath) {
404     if (filePath == null) {
405       VirtualFile virtualFile = myFile == null? null : myFile.getVirtualFile();
406       filePath = virtualFile == null? null : virtualFile.getUserData(VfsTestUtil.TEST_DATA_FILE_PATH);
407     }
408     String fileName = myFile == null ? "" : myFile.getName() + ": ";
409     String failMessage = "";
410
411     for (HighlightInfo info : reverseCollection(infos)) {
412       if (!expectedInfosContainsInfo(info) && !myIgnoreExtraHighlighting) {
413         final int startOffset = info.startOffset;
414         final int endOffset = info.endOffset;
415         String s = text.substring(startOffset, endOffset);
416         String desc = info.getDescription();
417
418         if (!failMessage.isEmpty()) failMessage += '\n';
419         failMessage += fileName + "Extra " +
420                           rangeString(text, startOffset, endOffset) +
421                           " :'" +
422                           s +
423                           "'" + (desc == null ? "" : " (" + desc + ")")
424                           + " [" + info.type + "]";
425       }
426     }
427
428     final Collection<ExpectedHighlightingSet> expectedHighlights = myHighlightingTypes.values();
429     for (ExpectedHighlightingSet highlightingSet : reverseCollection(expectedHighlights)) {
430       final Set<HighlightInfo> expInfos = highlightingSet.infos;
431       for (HighlightInfo expectedInfo : expInfos) {
432         if (!infosContainsExpectedInfo(infos, expectedInfo) && highlightingSet.enabled) {
433           final int startOffset = expectedInfo.startOffset;
434           final int endOffset = expectedInfo.endOffset;
435           String s = text.substring(startOffset, endOffset);
436           String desc = expectedInfo.getDescription();
437
438           if (!failMessage.isEmpty()) failMessage += '\n';
439           failMessage += fileName + "Missing " +
440                             rangeString(text, startOffset, endOffset) +
441                             " :'" +
442                             s +
443                             "'" + (desc == null ? "" : " (" + desc + ")");
444         }
445       }
446     }
447
448     if (!failMessage.isEmpty()) {
449       compareTexts(infos, text, failMessage + "\n", filePath);
450     }
451   }
452
453   private static <T> List<T> reverseCollection(Collection<T> infos) {
454     return ContainerUtil.reverse(infos instanceof List ? (List<T>)infos : new ArrayList<>(infos));
455   }
456
457   private void compareTexts(Collection<HighlightInfo> infos, String text, String failMessage, @Nullable String filePath) {
458     String actual = composeText(myHighlightingTypes, infos, text);
459     if (filePath != null && !myText.equals(actual)) {
460       // uncomment to overwrite, don't forget to revert on commit!
461       //VfsTestUtil.overwriteTestData(filePath, actual);
462       //return;
463       throw new FileComparisonFailure(failMessage, myText, actual, filePath);
464     }
465     Assert.assertEquals(failMessage + "\n", myText, actual);
466     Assert.fail(failMessage);
467   }
468
469   public static String composeText(final Map<String, ExpectedHighlightingSet> types, Collection<HighlightInfo> infos, String text) {
470     // filter highlighting data and map each highlighting to a tag name
471     List<Pair<String, HighlightInfo>> list = ContainerUtil.mapNotNull(infos,
472                                                                       (NullableFunction<HighlightInfo, Pair<String, HighlightInfo>>)info -> {
473                                                                         for (Map.Entry<String, ExpectedHighlightingSet> entry : types.entrySet()) {
474                                                                           final ExpectedHighlightingSet set = entry.getValue();
475                                                                           if (set.enabled && set.severity == info.getSeverity() && set.endOfLine == info.isAfterEndOfLine()) {
476                                                                             return Pair.create(entry.getKey(), info);
477                                                                           }
478                                                                         }
479                                                                         return null;
480                                                                       });
481
482     boolean showAttributesKeys = false;
483     for (ExpectedHighlightingSet eachSet : types.values()) {
484       for (HighlightInfo eachInfo : eachSet.infos) {
485         if (eachInfo.forcedTextAttributesKey != null) {
486           showAttributesKeys = true;
487           break;
488         }
489       }
490     }
491
492     // sort filtered highlighting data by end offset in descending order
493     Collections.sort(list, (o1, o2) -> {
494       HighlightInfo i1 = o1.second;
495       HighlightInfo i2 = o2.second;
496
497       int byEnds = i2.endOffset - i1.endOffset;
498       if (byEnds != 0) return byEnds;
499
500       if (!i1.isAfterEndOfLine() && !i2.isAfterEndOfLine()) {
501         int byStarts = i1.startOffset - i2.startOffset;
502         if (byStarts != 0) return byStarts;
503       }
504       else {
505         int byEOL = Comparing.compare(i2.isAfterEndOfLine(), i1.isAfterEndOfLine());
506         if (byEOL != 0) return byEOL;
507       }
508
509       int bySeverity = i2.getSeverity().compareTo(i1.getSeverity());
510       if (bySeverity != 0) return bySeverity;
511
512       return Comparing.compare(i1.getDescription(), i2.getDescription());
513     });
514
515     // combine highlighting data with original text
516     StringBuilder sb = new StringBuilder();
517     Couple<Integer> result = composeText(sb, list, 0, text, text.length(), 0, showAttributesKeys);
518     sb.insert(0, text.substring(0, result.second));
519     return sb.toString();
520   }
521
522   private static Couple<Integer> composeText(StringBuilder sb,
523                                              List<Pair<String, HighlightInfo>> list, int index,
524                                              String text, int endPos, int startPos, boolean showAttributesKeys) {
525     int i = index;
526     while (i < list.size()) {
527       Pair<String, HighlightInfo> pair = list.get(i);
528       HighlightInfo info = pair.second;
529       if (info.endOffset <= startPos) {
530         break;
531       }
532
533       String severity = pair.first;
534       HighlightInfo prev = i < list.size() - 1 ? list.get(i + 1).second : null;
535
536       sb.insert(0, text.substring(info.endOffset, endPos));
537       sb.insert(0, "</" + severity + ">");
538       endPos = info.endOffset;
539       if (prev != null && prev.endOffset > info.startOffset) {
540         Couple<Integer> result = composeText(sb, list, i + 1, text, endPos, info.startOffset, showAttributesKeys);
541         i = result.first - 1;
542         endPos = result.second;
543       }
544       sb.insert(0, text.substring(info.startOffset, endPos));
545
546       String str = "<" + severity + " descr=\"" + StringUtil.escapeQuotes(String.valueOf(info.getDescription())) + "\"";
547       if (showAttributesKeys) {
548         str += " textAttributesKey=\"" + info.forcedTextAttributesKey + "\"";
549       }
550       str += ">";
551       sb.insert(0, str);
552
553       endPos = info.startOffset;
554       i++;
555     }
556
557     return Couple.of(i, endPos);
558   }
559
560   private static boolean infosContainsExpectedInfo(Collection<HighlightInfo> infos, HighlightInfo expectedInfo) {
561     for (HighlightInfo info : infos) {
562       if (infoEquals(expectedInfo, info)) {
563         return true;
564       }
565     }
566     return false;
567   }
568
569   private boolean expectedInfosContainsInfo(HighlightInfo info) {
570     if (info.getTextAttributes(null, null) == TextAttributes.ERASE_MARKER) return true;
571     final Collection<ExpectedHighlightingSet> expectedHighlights = myHighlightingTypes.values();
572     for (ExpectedHighlightingSet highlightingSet : expectedHighlights) {
573       if (highlightingSet.severity != info.getSeverity()) continue;
574       if (!highlightingSet.enabled) return true;
575       final Set<HighlightInfo> infos = highlightingSet.infos;
576       for (HighlightInfo expectedInfo : infos) {
577         if (infoEquals(expectedInfo, info)) {
578           return true;
579         }
580       }
581     }
582     return false;
583   }
584
585   private static boolean infoEquals(HighlightInfo expectedInfo, HighlightInfo info) {
586     if (expectedInfo == info) return true;
587     return
588       info.getSeverity() == expectedInfo.getSeverity() &&
589       info.startOffset == expectedInfo.startOffset &&
590       info.endOffset == expectedInfo.endOffset &&
591       info.isAfterEndOfLine() == expectedInfo.isAfterEndOfLine() &&
592       (expectedInfo.type == WHATEVER || expectedInfo.type.equals(info.type)) &&
593       (Comparing.strEqual(ANY_TEXT, expectedInfo.getDescription()) || Comparing.strEqual(info.getDescription(), expectedInfo.getDescription())) &&
594       (expectedInfo.forcedTextAttributes == null || Comparing.equal(expectedInfo.getTextAttributes(null, null), info.getTextAttributes(null, null))) &&
595       (expectedInfo.forcedTextAttributesKey == null || expectedInfo.forcedTextAttributesKey.equals(info.forcedTextAttributesKey));
596   }
597
598   private static String rangeString(String text, int startOffset, int endOffset) {
599     int startLine = StringUtil.offsetToLineNumber(text, startOffset);
600     int endLine = StringUtil.offsetToLineNumber(text, endOffset);
601
602     int startCol = startOffset - StringUtil.lineColToOffset(text, startLine, 0);
603     int endCol = endOffset - StringUtil.lineColToOffset(text, endLine, 0);
604
605     if (startLine == endLine) {
606       return String.format("(%d:%d/%d)", startLine + 1, startCol + 1, endCol - startCol);
607     }
608     return String.format("(%d:%d..%d:%d)", startLine + 1, endLine + 1, startCol + 1, endCol + 1);
609   }
610
611 }