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