@@ -226,6 +226,7 @@ private static RangeMap<Integer, String> buildReplacements(
226226 Set <String > usedNames ,
227227 Multimap <String , Range <Integer >> usedInJavadoc ) {
228228 RangeMap <Integer , String > replacements = TreeRangeMap .create ();
229+ String sep = Newlines .guessLineSeparator (contents );
229230 for (JCImport importTree : unit .getImports ()) {
230231 String simpleName = getSimpleName (importTree );
231232 if (!isUnused (unit , usedNames , usedInJavadoc , importTree , simpleName )) {
@@ -234,16 +235,62 @@ private static RangeMap<Integer, String> buildReplacements(
234235 // delete the import
235236 int endPosition = importTree .getEndPosition (unit .endPositions );
236237 endPosition = Math .max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
237- String sep = Newlines .guessLineSeparator (contents );
238238 if (endPosition + sep .length () < contents .length ()
239239 && contents .subSequence (endPosition , endPosition + sep .length ())
240240 .toString ()
241241 .equals (sep )) {
242242 endPosition += sep .length ();
243243 }
244- replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
244+ // putCoalescing merges adjacent unused imports into one range, so the blank-line cleanup below sees the
245+ // whole deleted import block (TreeRangeMap.put does not coalesce).
246+ replacements .putCoalescing (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
245247 }
246- return replacements ;
248+ // Removing a whole import block can leave the blank line that preceded it stacked on the blank line that
249+ // followed it (package, blank, imports, blank, type). Collapse one of them, so a single formatting pass leaves
250+ // one blank line, as the second one did.
251+ return collapseBlankLinesAroundDeletedImports (contents , replacements , sep );
252+ }
253+
254+ /**
255+ * Extends contiguous deleted-import ranges so that a blank line that both preceded and followed the imports is not
256+ * left doubled after the deletion.
257+ */
258+ private static RangeMap <Integer , String > collapseBlankLinesAroundDeletedImports (
259+ String contents , RangeMap <Integer , String > replacements , String sep ) {
260+ if (replacements .asMapOfRanges ().isEmpty ()) {
261+ return replacements ;
262+ }
263+ RangeMap <Integer , String > adjusted = TreeRangeMap .create ();
264+ for (Range <Integer > range : replacements .asMapOfRanges ().keySet ()) {
265+ int start = range .lowerEndpoint ();
266+ int end = range .upperEndpoint ();
267+ // Eat one trailing blank line when the deletion sits between blank lines, or at the start of the file,
268+ // where a leading blank line would otherwise remain after the last import is removed.
269+ if (isBlankLineAfter (contents , end , sep ) && (start == 0 || isBlankLineBefore (contents , start , sep ))) {
270+ end += sep .length ();
271+ }
272+ adjusted .putCoalescing (Range .closedOpen (start , end ), "" );
273+ }
274+ return adjusted ;
275+ }
276+
277+ /** True if {@code pos} is immediately preceded by an empty line. */
278+ private static boolean isBlankLineBefore (String contents , int pos , String sep ) {
279+ if (pos < sep .length () || !contents .regionMatches (pos - sep .length (), sep , 0 , sep .length ())) {
280+ return false ;
281+ }
282+ int endOfPreviousLine = pos - sep .length ();
283+ if (endOfPreviousLine == 0 ) {
284+ // The file begins with a blank line before the deleted import.
285+ return true ;
286+ }
287+ return endOfPreviousLine >= sep .length ()
288+ && contents .regionMatches (endOfPreviousLine - sep .length (), sep , 0 , sep .length ());
289+ }
290+
291+ /** True if {@code pos} is immediately followed by an empty line (a line break). */
292+ private static boolean isBlankLineAfter (String contents , int pos , String sep ) {
293+ return pos + sep .length () <= contents .length () && contents .regionMatches (pos , sep , 0 , sep .length ());
247294 }
248295
249296 private static String getSimpleName (ImportTree importTree ) {
0 commit comments