@Nullable
  private static StringKind findStringKind(@NotNull PsiFile file, int offset) {
    final PsiElement leaf = file.findElementAt(offset);
    if (leaf == null) {
      return null;
    }
    final IElementType leafType = leaf.getNode().getElementType();
    final int leafOffset = leaf.getTextOffset();
    if (offset == leafOffset) {
      // special cases
      if (leafType == GSTRING_END || leafType == SLASHY_END || leafType == DOLLAR_SLASHY_END) {
        PsiElement prevSibling = leaf.getPrevSibling();
        if (prevSibling != null) {
          IElementType previousElementType = prevSibling.getNode().getElementType();
          if (previousElementType == STRING_INJECTION) {
            if (leafType == GSTRING_END) {
              if (isMultiline(leaf.getParent())) {
                // """...${}<here>"""
                return StringKind.TRIPLE_DOUBLE_QUOTED;
              }
              else {
                // "...${}<here>"
                return StringKind.DOUBLE_QUOTED;
              }
            }
            if (leafType == SLASHY_END) {
              // /...${}<here>/
              return StringKind.SLASHY;
            }
            else {
              // $/...${}<here>/$
              return StringKind.DOLLAR_SLASHY;
            }
          }
          else if (previousElementType == STRING_CONTENT) {
            // "...${}...<here>"
            // """...${}...<here>"""
            // /...${}...<here>/
            // $/...${}...<here>/$
            PsiElement contentLeaf = prevSibling.getFirstChild();
            return getStringKindByContentTokenType(contentLeaf, contentLeaf.getNode().getElementType());
          }
          else if (previousElementType == DOLLAR_SLASHY_BEGIN) {
            // $/<here>/$
            return StringKind.DOLLAR_SLASHY;
          }
          else if (leafType != GSTRING_END) {
            // /...<here>/
            // $/...<here>/$
            //
            // "..." is a single token returned by lexer,
            // while slashy and dollar slashy strings without injections consist of three tokens start quote, content and end quote
            return getStringKindByContentTokenType(leaf, previousElementType);
          }
        }
      }
      else if (leafType == T_DOLLAR) {
        // Strings with injections, leaf is dollar:
        // "<here>${}..." or "...<here>${}..."
        // /<here>${}.../ or /...<here>${}.../
        // $/<here>${}.../$ or $/...<here>${}.../$
        PsiElement parent = leaf.getParent(); // template string injection
        PsiElement gParent = parent.getParent(); // template string element;
        return getStringKindByStringElement(gParent);
      }
    }
    final int leafEndOffset = leafOffset + leaf.getTextLength();
    if (leafType == STRING_SQ || leafType == STRING_DQ) {
      if (leafOffset + 1 <= offset && offset <= leafEndOffset - 1) {
        // Single token literals:
        // '<here>......'
        // '...<here>...'
        // '......<here>'
        // "<here>......"
        // "...<here>..."
        // "......<here>"
        return leafType == STRING_SQ ? StringKind.SINGLE_QUOTED : StringKind.DOUBLE_QUOTED;
      }
    }
    if (leafType == STRING_TSQ || leafType == STRING_TDQ) {
      // Single token literals:
      // '''<here>...'''
      // """<here>..."""
      if (leafOffset + 3 <= offset && offset <= leafEndOffset - 3) {
        return leafType == STRING_TSQ ? StringKind.TRIPLE_SINGLE_QUOTED : StringKind.TRIPLE_DOUBLE_QUOTED;
      }
    }

    // Strings with injections, leaf is string content:
    // "<here>...${}..." or "...<here>...${}..."
    // """<here>...${}...""" or """...<here>...${}..."""
    // /<here>...${}.../ or /...<here>...${}.../
    // $/<here>...${}.../$ or $/...<here>...${}.../$
    return getStringKindByContentTokenType(leaf, leafType);
  }
@Nullable
  private static StringKind findStringKind(@NotNull PsiFile file, int offset)