// returns startOffset found, or -1 if need to continue searching
 private static int countNewLinesIn(CharSequence text, int[] pos, int line) {
   int offsetInside = 0;
   for (int i = StringUtil.indexOf(text, '\n');
       i != -1;
       i = StringUtil.indexOf(text, '\n', offsetInside)) {
     int curLine = ++pos[0];
     int lineLength = i + 1 - offsetInside;
     int offset = pos[1] += lineLength;
     offsetInside += lineLength;
     if (curLine == line) return offset;
   }
   pos[1] += text.length() - offsetInside;
   return -1;
 }
 @Nullable
 private static String getPropertyName(@NotNull Document document, int line) {
   int startOffset = document.getLineStartOffset(line);
   int endOffset =
       StringUtil.indexOf(
           document.getCharsSequence(), '=', startOffset, document.getLineEndOffset(line));
   if (endOffset <= startOffset) {
     return null;
   }
   String propertyName =
       document.getCharsSequence().subSequence(startOffset, endOffset).toString().trim();
   return propertyName.isEmpty() ? null : propertyName;
 }
 public static boolean isHashBangLine(CharSequence firstCharsIfText, String marker) {
   if (firstCharsIfText == null) {
     return false;
   }
   final int lineBreak = StringUtil.indexOf(firstCharsIfText, '\n');
   if (lineBreak < 0) {
     return false;
   }
   String firstLine = firstCharsIfText.subSequence(0, lineBreak).toString();
   if (!firstLine.startsWith("#!")) {
     return false;
   }
   return firstLine.contains(marker);
 }
    public void replace(int initialStart, int length, String replace) {
      // calculating fragment
      // minimize replace
      int start = 0;
      int end = start + length;

      final int replaceLength = replace.length();
      final String chars = getText(start + initialStart, end + initialStart);
      if (chars.equals(replace)) return;

      int newStartInReplace = 0;
      int newEndInReplace = replaceLength;
      while (newStartInReplace < replaceLength
          && start < end
          && replace.charAt(newStartInReplace) == chars.charAt(start)) {
        start++;
        newStartInReplace++;
      }

      while (start < end
          && newStartInReplace < newEndInReplace
          && replace.charAt(newEndInReplace - 1) == chars.charAt(end - 1)) {
        newEndInReplace--;
        end--;
      }

      // optimization: when delete fragment from the middle of the text, prefer split at the line
      // boundaries
      if (newStartInReplace == newEndInReplace
          && start > 0
          && start < end
          && StringUtil.indexOf(chars, '\n', start, end) != -1) {
        // try to align to the line boundaries
        while (start > 0
            && newStartInReplace > 0
            && chars.charAt(start - 1) == chars.charAt(end - 1)
            && chars.charAt(end - 1) != '\n') {
          start--;
          end--;
          newStartInReplace--;
          newEndInReplace--;
        }
      }

      // [mike] dirty hack for xml:
      // make sure that deletion of <t> in: <tag><t/><tag> doesn't remove t/><
      // which is perfectly valid but invalidates range markers
      start += initialStart;
      end += initialStart;
      final CharSequence charsSequence = myDocument.getCharsSequence();
      while (start < charsSequence.length()
          && end < charsSequence.length()
          && start > 0
          && charsSequence.subSequence(start, end).toString().endsWith("><")
          && charsSequence.charAt(start - 1) == '<') {
        start--;
        newStartInReplace--;
        end--;
        newEndInReplace--;
      }

      replace = replace.substring(newStartInReplace, newEndInReplace);
      length = end - start;

      final Pair<MutableTextRange, StringBuffer> fragment = getFragmentByRange(start, length);
      final StringBuffer fragmentReplaceText = fragment.getSecond();
      final int startInFragment = start - fragment.getFirst().getStartOffset();

      // text range adjustment
      final int lengthDiff = replace.length() - length;
      final Iterator<Pair<MutableTextRange, StringBuffer>> iterator =
          myAffectedFragments.iterator();
      boolean adjust = false;
      while (iterator.hasNext()) {
        final Pair<MutableTextRange, StringBuffer> pair = iterator.next();
        if (adjust) pair.getFirst().shift(lengthDiff);
        if (pair == fragment) adjust = true;
      }

      fragmentReplaceText.replace(startInFragment, startInFragment + length, replace);
    }
    public void replace(
        int psiStart, int length, @NotNull String replace, @Nullable PsiElement replacement) {
      // calculating fragment
      // minimize replace
      int start = 0;
      int end = start + length;

      final CharSequence chars = myPsiText.subSequence(psiStart, psiStart + length);
      if (StringUtil.equals(chars, replace)) return;

      int newStartInReplace = 0;
      final int replaceLength = replace.length();
      while (newStartInReplace < replaceLength
          && start < end
          && replace.charAt(newStartInReplace) == chars.charAt(start)) {
        start++;
        newStartInReplace++;
      }

      int newEndInReplace = replaceLength;
      while (start < end
          && newStartInReplace < newEndInReplace
          && replace.charAt(newEndInReplace - 1) == chars.charAt(end - 1)) {
        newEndInReplace--;
        end--;
      }

      // increase the changed range to start and end on PSI token boundaries
      // this will help to survive smart pointers with the same boundaries
      if (replacement != null && (newStartInReplace > 0 || newEndInReplace < replaceLength)) {
        PsiElement startLeaf = replacement.findElementAt(newStartInReplace);
        PsiElement endLeaf = replacement.findElementAt(newEndInReplace - 1);
        if (startLeaf != null && endLeaf != null) {
          int leafStart =
              startLeaf.getTextRange().getStartOffset()
                  - replacement.getTextRange().getStartOffset();
          int leafEnd =
              endLeaf.getTextRange().getEndOffset() - replacement.getTextRange().getStartOffset();
          start += leafStart - newStartInReplace;
          end += leafEnd - newEndInReplace;
          newStartInReplace = leafStart;
          newEndInReplace = leafEnd;
        }
      }

      // optimization: when delete fragment from the middle of the text, prefer split at the line
      // boundaries
      if (newStartInReplace == newEndInReplace
          && start > 0
          && start < end
          && StringUtil.indexOf(chars, '\n', start, end) != -1) {
        // try to align to the line boundaries
        while (start > 0
            && newStartInReplace > 0
            && chars.charAt(start - 1) == chars.charAt(end - 1)
            && chars.charAt(end - 1) != '\n') {
          start--;
          end--;
          newStartInReplace--;
          newEndInReplace--;
        }
      }

      start += psiStart;
      end += psiStart;

      // [mike] dirty hack for xml:
      // make sure that deletion of <t> in: <tag><t/><tag> doesn't remove t/><
      // which is perfectly valid but invalidates range markers
      final CharSequence charsSequence = myPsiText;
      while (start < charsSequence.length()
          && end < charsSequence.length()
          && start > 0
          && charsSequence.subSequence(start, end).toString().endsWith("><")
          && charsSequence.charAt(start - 1) == '<') {
        start--;
        newStartInReplace--;
        end--;
        newEndInReplace--;
      }

      updateFragments(start, end, replace.substring(newStartInReplace, newEndInReplace));
    }
  public int matchingDegree(@NotNull String name) {
    FList<TextRange> iterable = matchingFragments(name);
    if (iterable == null) return Integer.MIN_VALUE;
    if (iterable.isEmpty()) return 0;

    final TextRange first = iterable.getHead();
    boolean startMatch = first.getStartOffset() == 0;

    int matchingCase = 0;
    int p = -1;

    int integral =
        0; // -sum of matching-char-count * hump-index over all matched humps; favors longer
           // fragments matching earlier words
    int humpIndex = 1;
    int nextHumpStart = 0;
    for (TextRange range : iterable) {
      for (int i = range.getStartOffset(); i < range.getEndOffset(); i++) {
        boolean isHumpStart = false;
        while (nextHumpStart <= i) {
          if (nextHumpStart == i) {
            isHumpStart = true;
          }
          nextHumpStart = NameUtil.nextWord(name, nextHumpStart);
          if (first != range) {
            humpIndex++;
          }
        }
        integral -= humpIndex;

        char c = name.charAt(i);
        p = StringUtil.indexOf(myPattern, c, p + 1, myPattern.length, false);
        if (p < 0) {
          break;
        }

        if (c == myPattern[p]) {
          if (isUpperCase[p])
            matchingCase +=
                50; // strongly prefer user's uppercase matching uppercase: they made an effort to
                    // press Shift
          else if (i == 0 && startMatch)
            matchingCase += 15; // the very first letter case distinguishes classes in Java etc
          else if (isHumpStart)
            matchingCase +=
                1; // if a lowercase matches lowercase hump start, that also means something
        } else if (isHumpStart) {
          // disfavor hump starts where pattern letter case doesn't match name case
          matchingCase -= 20;
        }
      }
    }

    int startIndex = first.getStartOffset();
    boolean afterSeparator = StringUtil.indexOfAny(name, HARD_SEPARATORS, 0, startIndex) >= 0;
    boolean wordStart =
        startIndex == 0 || isWordStart(name, startIndex) && !isWordStart(name, startIndex - 1);
    boolean finalMatch = iterable.get(iterable.size() - 1).getEndOffset() == name.length();

    return (wordStart ? 1000 : 0)
        + integral * 10
        + matchingCase * (startMatch ? 10 : 1)
        + // in start matches, case is more important; in middle matches - fragment length
          // (integral)
        (afterSeparator ? 0 : 2)
        + (finalMatch ? 1 : 0);
  }