private static Couple<Integer> composeText(
      StringBuilder sb,
      List<Pair<String, HighlightInfo>> list,
      int index,
      String text,
      int endPos,
      int startPos) {
    int i = index;
    while (i < list.size()) {
      Pair<String, HighlightInfo> pair = list.get(i);
      HighlightInfo info = pair.second;
      if (info.endOffset <= startPos) {
        break;
      }

      String severity = pair.first;
      HighlightInfo prev = i < list.size() - 1 ? list.get(i + 1).second : null;

      sb.insert(0, text.substring(info.endOffset, endPos));
      sb.insert(0, "</" + severity + ">");
      endPos = info.endOffset;
      if (prev != null && prev.endOffset > info.startOffset) {
        Couple<Integer> result = composeText(sb, list, i + 1, text, endPos, info.startOffset);
        i = result.first - 1;
        endPos = result.second;
      }
      sb.insert(0, text.substring(info.startOffset, endPos));
      sb.insert(0, "<" + severity + " descr=\"" + info.getDescription() + "\">");

      endPos = info.startOffset;
      i++;
    }

    return Couple.newOne(i, endPos);
  }
  private static Couple<Integer> composeText(
      StringBuilder sb,
      List<Pair<String, HighlightInfo>> list,
      int index,
      String text,
      int endPos,
      int startPos,
      boolean showAttributesKeys) {
    int i = index;
    while (i < list.size()) {
      Pair<String, HighlightInfo> pair = list.get(i);
      HighlightInfo info = pair.second;
      if (info.endOffset <= startPos) {
        break;
      }

      String severity = pair.first;
      HighlightInfo prev = i < list.size() - 1 ? list.get(i + 1).second : null;

      sb.insert(0, text.substring(info.endOffset, endPos));
      sb.insert(0, "</" + severity + ">");
      endPos = info.endOffset;
      if (prev != null && prev.endOffset > info.startOffset) {
        Couple<Integer> result =
            composeText(sb, list, i + 1, text, endPos, info.startOffset, showAttributesKeys);
        i = result.first - 1;
        endPos = result.second;
      }
      sb.insert(0, text.substring(info.startOffset, endPos));

      String str =
          "<"
              + severity
              + " descr=\""
              + StringUtil.escapeQuotes(String.valueOf(info.getDescription()))
              + "\"";
      if (showAttributesKeys) {
        str += " textAttributesKey=\"" + info.forcedTextAttributesKey + "\"";
      }
      str += ">";
      sb.insert(0, str);

      endPos = info.startOffset;
      i++;
    }

    return Couple.of(i, endPos);
  }
  public static String composeText(
      final Map<String, ExpectedHighlightingSet> types,
      Collection<HighlightInfo> infos,
      String text) {
    // filter highlighting data and map each highlighting to a tag name
    List<Pair<String, HighlightInfo>> list =
        ContainerUtil.mapNotNull(
            infos,
            new NullableFunction<HighlightInfo, Pair<String, HighlightInfo>>() {
              @Override
              public Pair<String, HighlightInfo> fun(HighlightInfo info) {
                for (Map.Entry<String, ExpectedHighlightingSet> entry : types.entrySet()) {
                  final ExpectedHighlightingSet set = entry.getValue();
                  if (set.enabled
                      && set.severity == info.getSeverity()
                      && set.endOfLine == info.isAfterEndOfLine()) {
                    return Pair.create(entry.getKey(), info);
                  }
                }
                return null;
              }
            });

    // sort filtered highlighting data by end offset in descending order
    Collections.sort(
        list,
        new Comparator<Pair<String, HighlightInfo>>() {
          @Override
          public int compare(Pair<String, HighlightInfo> o1, Pair<String, HighlightInfo> o2) {
            HighlightInfo i1 = o1.second;
            HighlightInfo i2 = o2.second;

            int byEnds = i2.endOffset - i1.endOffset;
            if (byEnds != 0) return byEnds;

            if (!i1.isAfterEndOfLine() && !i2.isAfterEndOfLine()) {
              int byStarts = i1.startOffset - i2.startOffset;
              if (byStarts != 0) return byStarts;
            } else {
              int byEOL = Comparing.compare(i2.isAfterEndOfLine(), i1.isAfterEndOfLine());
              if (byEOL != 0) return byEOL;
            }

            int bySeverity = i2.getSeverity().compareTo(i1.getSeverity());
            if (bySeverity != 0) return bySeverity;

            return Comparing.compare(i1.getDescription(), i2.getDescription());
          }
        });

    // combine highlighting data with original text
    StringBuilder sb = new StringBuilder();
    Couple<Integer> result = composeText(sb, list, 0, text, text.length(), 0);
    sb.insert(0, text.substring(0, result.second));
    return sb.toString();
  }
  public static String composeText(
      final Map<String, ExpectedHighlightingSet> types,
      Collection<HighlightInfo> infos,
      String text) {
    // filter highlighting data and map each highlighting to a tag name
    List<Pair<String, HighlightInfo>> list =
        ContainerUtil.mapNotNull(
            infos,
            (NullableFunction<HighlightInfo, Pair<String, HighlightInfo>>)
                info -> {
                  for (Map.Entry<String, ExpectedHighlightingSet> entry : types.entrySet()) {
                    final ExpectedHighlightingSet set = entry.getValue();
                    if (set.enabled
                        && set.severity == info.getSeverity()
                        && set.endOfLine == info.isAfterEndOfLine()) {
                      return Pair.create(entry.getKey(), info);
                    }
                  }
                  return null;
                });

    boolean showAttributesKeys = false;
    for (ExpectedHighlightingSet eachSet : types.values()) {
      for (HighlightInfo eachInfo : eachSet.infos) {
        if (eachInfo.forcedTextAttributesKey != null) {
          showAttributesKeys = true;
          break;
        }
      }
    }

    // sort filtered highlighting data by end offset in descending order
    Collections.sort(
        list,
        (o1, o2) -> {
          HighlightInfo i1 = o1.second;
          HighlightInfo i2 = o2.second;

          int byEnds = i2.endOffset - i1.endOffset;
          if (byEnds != 0) return byEnds;

          if (!i1.isAfterEndOfLine() && !i2.isAfterEndOfLine()) {
            int byStarts = i1.startOffset - i2.startOffset;
            if (byStarts != 0) return byStarts;
          } else {
            int byEOL = Comparing.compare(i2.isAfterEndOfLine(), i1.isAfterEndOfLine());
            if (byEOL != 0) return byEOL;
          }

          int bySeverity = i2.getSeverity().compareTo(i1.getSeverity());
          if (bySeverity != 0) return bySeverity;

          return Comparing.compare(i1.getDescription(), i2.getDescription());
        });

    // combine highlighting data with original text
    StringBuilder sb = new StringBuilder();
    Couple<Integer> result = composeText(sb, list, 0, text, text.length(), 0, showAttributesKeys);
    sb.insert(0, text.substring(0, result.second));
    return sb.toString();
  }