private static void replace(@NotNull StringBuilder text,
                              @NotNull String replaceFrom,
                              @NotNull String replaceTo,
                              @NotNull List<TextRange> readOnlyChanges) {

    if (replaceFrom.isEmpty()) {
      return;
    }

    for (int i = text.indexOf(replaceFrom); i >= 0; i = text.indexOf(replaceFrom, i + 1)) {
      int end = i + replaceFrom.length();
      if (intersects(readOnlyChanges, i, end)) {
        continue;
      }
      if (!"\n".equals(replaceFrom)) {
        if (end < text.length() && !ALLOWED_LINK_SEPARATORS.contains(text.charAt(end))) {
          // Consider a situation when we have, say, replacement from text 'PsiType' and encounter a 'PsiTypeParameter' in the text.
          // We don't want to perform the replacement then.
          continue;
        }
        if (i > 0 && !ALLOWED_LINK_SEPARATORS.contains(text.charAt(i - 1))) {
          // Similar situation but targets head match: from = 'TextRange', text = 'getTextRange()'.
          continue;
        }
      }
      text.replace(i, end, replaceTo);
      int diff = replaceTo.length() - replaceFrom.length();
      for (int j = 0; j < readOnlyChanges.size(); j++) {
        TextRange range = readOnlyChanges.get(j);
        if (range.getStartOffset() >= end) {
          readOnlyChanges.set(j, range.shiftRight(diff));
        }
      }
      readOnlyChanges.add(new TextRange(i, i + replaceTo.length()));
    }
  }
private static void replace(@NotNull StringBuilder text,
                              @NotNull String replaceFrom,
                              @NotNull String replaceTo,
                              @NotNull List<TextRange> readOnlyChanges)