@Contract("_, _, false -> !null")
  public static @NlsSafe @SystemIndependent String computeRelativePath(@NotNull @SystemIndependent String base,
                                                                       @NotNull @SystemIndependent String shifted,
                                                                       boolean relativePathsOnly) {
    if (stringEqual(shifted, base)) {
      return ".";
    }
    String[] baseParts = base.split("/");
    String[] shiftedParts = shifted.split("/");

    int cnt = 0;
    while (true) {
      if (baseParts.length <= cnt || shiftedParts.length <= cnt) {
        // means that directory moved to a file or vise versa -> undo last match
        cnt--;
        break;
      }
      if (!stringEqual(baseParts[cnt], shiftedParts[cnt])) {
        break;
      }
      ++cnt;
    }

    int stepsUp = baseParts.length - cnt - 1;
    int stepsDown = shiftedParts.length - cnt - 1;

    if (stepsDown > MAX_STEPS_DOWN) {
      return relativePathsOnly ? null : shifted;
    }

    StringBuilder sb = new StringBuilder();
    if (stepsUp < MAX_STEPS_UP) {
      for (int i = 0; i < stepsUp; i++) {
        sb.append("../");
      }
    }
    else {
      sb.append(StringUtil.ELLIPSIS + "/");
    }

    for (int i = cnt; i < shiftedParts.length - 1; i++) {
      final String shiftedPart = shiftedParts[i];
      sb.append(shiftedPart);
      sb.append('/');
    }

    String oldName = baseParts[baseParts.length - 1];
    String newName = shiftedParts[shiftedParts.length - 1];
    boolean skipFileName = oldName.equals(newName);
    if (!skipFileName) {
      sb.append(newName);
    }

    return sb.toString();
  }
@Contract("_, _, false -> !null")
  public static @NlsSafe @SystemIndependent String computeRelativePath(@NotNull @SystemIndependent String base,
                                                                       @NotNull @SystemIndependent String shifted,
                                                                       boolean relativePathsOnly)