@NotNull
  private Field parseField(int startOffset, int recursionDepth) {
    assert myNodeText.charAt(startOffset) == '{';

    int autoFieldNumber = myImplicitlyNumberedFieldsCounter;

    // in the order of appearance inside a field
    final IntList attrAndLookupBounds=new IntArrayList();
    int conversionStart = -1;
    int formatSpecStart = -1;
    final List<Field> nestedFields = new ArrayList<>();
    int rightBraceOffset = -1;

    boolean insideItem = false;
    boolean recovering = false;

    final int contentEnd = myNodeContentRange.getEndOffset();
    int offset = startOffset + 1;
    while (offset < contentEnd) {
      final int nextOffset = skipNamedUnicodeEscape(offset);
      if (offset != nextOffset) {
        offset = nextOffset;
        continue;
      }

      final char c = myNodeText.charAt(offset);
      // inside "name" part of the field
      if (conversionStart == -1 && formatSpecStart == -1) {
        // '{' can appear inside a lookup item, but everywhere else it means that field ends
        if (insideItem) {
          // inside lookup item skip everything up to the closing bracket
          if (c == ']') {
            insideItem = false;
            // remember the end offset of the lookup now, since later we may enter "recovering" state
            attrAndLookupBounds.add(offset + 1);
            // if the next character is neither '.', not '[' stop matching attributes and lookups here
            recovering = offset + 1 < contentEnd && !isAnyCharOf(myNodeText.charAt(offset + 1), ".[");
          }
        }
        else if (isAnyCharOf(c, "[.:!}")) {
          insideItem = c == '[';
          if (!recovering) {
            // avoid duplicate offsets in sequences like "]." or "]["
            addIfNotLastItem(attrAndLookupBounds, offset);

            // no name in the field, increment implicitly named fields counter
            if (attrAndLookupBounds.size() == 1 && attrAndLookupBounds.getInt(0) == startOffset + 1) {
              myImplicitlyNumberedFieldsCounter++;
            }
          }

          if (c == ':') {
            formatSpecStart = offset;
          }
          else if (c == '!') {
            conversionStart = offset;
          }
          else if (c == '}') {
            rightBraceOffset = offset;
            break;
          }
        }
      }
      else if (c == '}') {
        rightBraceOffset = offset;
        break;
      }
      else if (conversionStart >= 0) {
        if (c == ':') {
          formatSpecStart = offset;
        }
      }
      else {
        assert formatSpecStart >= 0;
        if (c == '{') {
          final Field field = parseField(offset, recursionDepth + 1);
          nestedFields.add(field);
          offset = field.getFieldEnd();
          continue;
        }
      }
      offset++;
    }

    // finish with the trailing attribute or the first name if the field ended unexpectedly
    if (offset >= contentEnd && conversionStart == -1 && formatSpecStart == -1 && !insideItem && !recovering) {
      addIfNotLastItem(attrAndLookupBounds, contentEnd);
    }

    assert !attrAndLookupBounds.isEmpty();

    return new Field(myNodeText,
                     startOffset,
                     attrAndLookupBounds.toIntArray(),
                     conversionStart,
                     formatSpecStart,
                     nestedFields,
                     rightBraceOffset,
                     rightBraceOffset == -1 ? contentEnd : rightBraceOffset + 1,
                     autoFieldNumber,
                     recursionDepth);
  }
@NotNull
  private Field parseField(int startOffset, int recursionDepth)