@Nullable
  public TextContent build(PsiElement root, TextContent.TextDomain domain, TextRange valueRange) {
    TextRange rootRange = root.getTextRange();
    if (isWrongRange(valueRange, rootRange.getLength())) {
      LOG.error("The range " + valueRange + " is out of the PSI element, length " + rootRange.getLength());
      return null;
    }

    int rootStart = rootRange.getStartOffset();
    TextRange fileValueRange = valueRange.shiftRight(rootStart);
    CharSequence fileText = root.getContainingFile().getViewProvider().getContents();
    if (isWrongRange(fileValueRange, fileText.length())) {
      LOG.error("The range " + fileValueRange + " is out of the file, length " + fileText.length());
      return null;
    }

    TextContent content = new PsiRecursiveElementWalkingVisitor() {
      final List<TextContentImpl.TokenInfo> tokens = new ArrayList<>();
      int currentStart = fileValueRange.getStartOffset();

      @Override
      public void visitElement(@NotNull PsiElement element) {
        TextRange range = element.getTextRange().intersection(fileValueRange);
        if (range == null) return;

        @Nullable ExclusionKind kind = classifier.apply(element);
        if (kind == ExclusionKind.exclude) {
          exclusionStarted(range);
        }
        else if (kind != null) {
          exclusionStarted(range);
          var tokenKind = kind == ExclusionKind.markup ? TextContentImpl.TokenKind.markup : TextContentImpl.TokenKind.unknown;
          tokens.add(new TextContentImpl.PsiToken("", root, TextRange.from(range.getStartOffset(), 0).shiftLeft(rootStart), tokenKind));
        }
        else {
          super.visitElement(element);
        }
      }

      private void exclusionStarted(TextRange range) {
        if (range.getStartOffset() != currentStart) {
          TextRange tokenRange = new TextRange(currentStart, range.getStartOffset());
          String tokenText = tokenRange.subSequence(fileText).toString();
          tokens.add(new TextContentImpl.PsiToken(tokenText, root, tokenRange.shiftLeft(rootStart), TextContentImpl.TokenKind.text));
        }
        currentStart = range.getEndOffset();
      }

      @Nullable TextContent walkPsiTree() {
        root.accept(this);
        exclusionStarted(TextRange.from(fileValueRange.getEndOffset(), 0));
        return tokens.isEmpty() ? null : new TextContentImpl(domain, tokens);
      }
    }.walkPsiTree();
    return content == null ? null : content.removeIndents(indentChars).removeLineSuffixes(suffixChars).trimWhitespace();
  }
@Nullable
  public TextContent build(PsiElement root, TextContent.TextDomain domain, TextRange valueRange)