private void reformatGroup(NewlineGroup group, List<Integer> offsetMapping, int extraIndent) {
    int offset = offsetMapping.get(group.myStart);
    int actualStart = group.myStart + offset;
    // Find the last newline before the start of this group, so we know how long the line already is
    int lastNewline = myStringBuilder.lastIndexOf(myLineSeparator, actualStart);
    int nextNewline = myStringBuilder.indexOf(myLineSeparator, actualStart);
    // Find the end of the first line of this group, if the group were to not be reformatted
    int firstPartEnd = nextNewline == -1 ? actualStart + group.myLength : Math.min(nextNewline, actualStart + group.myLength);
    // Go through all lines in the group, and find the longest one (without reformatting)
    int groupEndWithoutNewlines = lastNewline == -1 ? firstPartEnd : firstPartEnd - lastNewline;
    while (nextNewline != -1 && nextNewline <= actualStart + group.myLength) {
      int lineStart = nextNewline;
      int lineEnd = nextNewline = myStringBuilder.indexOf(myLineSeparator, nextNewline + 1);
      if (lineEnd == -1 || lineEnd > actualStart + group.myLength) {
        lineEnd = actualStart + group.myLength;
      }
      int lineLength = extraIndent + lineEnd - lineStart - myLineSeparator.length();
      if (lineLength > groupEndWithoutNewlines) {
        groupEndWithoutNewlines = lineLength;
      }
    }
    // If the longest line is longer than the preferred line length, then reformat the group
    boolean addNewLines = groupEndWithoutNewlines > myPreferredLineLength;

    int originalExtraIndent = extraIndent;
    // If we're reformatting this group, and not only its children, then apply extra indentation
    if (addNewLines && !group.myReplacements.isEmpty()) {
      extraIndent += group.myExtraIndent;
    }

    // Iterate over the children and replacements in one go rather than separately, because offsets need to be applied
    // from left to right
    int childrenIndex = 0;
    int replacementIndex = 0;
    for (int pos = group.myStart; pos <= group.myStart + group.myLength; pos++) {
      if (pos != group.myStart) {
        offsetMapping.add(offset);
      }
      assert offsetMapping.size() == pos + 1;

      // add extra indent after newlines
      if (pos + offset + myLineSeparator.length() < myStringBuilder.length() && myStringBuilder.substring(pos + offset, pos + offset + myLineSeparator.length()).equals(myLineSeparator)) {
        // not for blank lines
        if (!(pos + offset + myLineSeparator.length() * 2 < myStringBuilder.length()) || !myStringBuilder.substring(pos + offset + myLineSeparator.length(), pos + offset + myLineSeparator.length() * 2).equals(myLineSeparator)) {
          for (int i = 0; i < extraIndent; i++) {
            myStringBuilder.insert(pos + offset + myLineSeparator.length(), myIndent);
          }
          offset += myIndent.length() * extraIndent;
        }
      }

      // do multiple passes in an inner loop, as there could be arbitrarily many with the same offset
      // or replacements with offsets equal to the end position of a child group
      boolean anotherPass = true;
      while (anotherPass) {
        anotherPass = false;

        // replace replaceables with newlines
        if (addNewLines && replacementIndex < group.myReplacements.size() && pos == group.myReplacements.get(replacementIndex).myStart) {
          NewlineGroup.Replacement replacement = group.myReplacements.get(replacementIndex);
          myStringBuilder.replace(pos + offset, pos + offset + replacement.myLength, myLineSeparator);
          if (replacement.myDedent) {
            extraIndent = originalExtraIndent;
          }
          for (int i = 0; i < group.myBaseIndent + extraIndent; i++) {
            myStringBuilder.insert(pos + offset + myLineSeparator.length(), myIndent);
          }
          offset += myIndent.length() * (group.myBaseIndent + extraIndent) + myLineSeparator.length() - replacement.myLength;
          replacementIndex++;
          anotherPass = true;
        }
        // offset may have changed, update the offset mapping here because the child groups rely on the value being
        // correct in the list
        offsetMapping.set(offsetMapping.size() - 1, offset);

        // recursively iterate through child groups
        int currentPos = pos;
        if (childrenIndex < group.myChildren.size() && group.myChildren.get(childrenIndex).myStart == currentPos) {
          NewlineGroup child = group.myChildren.get(childrenIndex);
          reformatGroup(child, offsetMapping, extraIndent);
          offset = offsetMapping.get(offsetMapping.size() - 1);
          pos += child.myLength;
          childrenIndex++;
          anotherPass = true;
        }
      }
    }

    // update the offset value here because it might have changed when adding indents after existing newlines
    // the parent relies on the value being correct in the list
    offsetMapping.set(offsetMapping.size() - 1, offset);

    for (TextToken token : group.myTokens) {
      if (token.getStart() < offsetMapping.size()) {
        token.shift(offsetMapping.get(token.getStart()));
      }
    }
  }
Recursive reformatting of groups.
If the group's content would take you over the specified preferred line length, then the group is applied.

- This algorithm usually ensures that if a group is applied, then all the group's parents in the tree are also
  applied. This tends to produce a nice structured output with well-placed potential newlines.
- The formatter keeps track of the offset as a result of the reformat at each character in the text buffer, and
  applies them to the bytecode offsets afterwards, so that the line number mappings don't get messed up.
src/org/jetbrains/java/decompiler/util/TextBuffer.java