[analysis_server] Fix stack overflow when generating minimal edits for LSP range formatting

Fixes https://github.com/dart-lang/sdk/issues/47702.

Change-Id: I221427cb78eb6346fe6c78bf09c41bb22c78e8d2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/220324
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/source_edits.dart b/pkg/analysis_server/lib/src/lsp/source_edits.dart
index 2a5834e..49b1b09 100644
--- a/pkg/analysis_server/lib/src/lsp/source_edits.dart
+++ b/pkg/analysis_server/lib/src/lsp/source_edits.dart
@@ -180,7 +180,10 @@
       // To handle this, if both unformatted/formatted contain at least one
       // newline, split this change into two around the last newline so that the
       // final part (likely leading whitespace) can be included without
-      // including the whole change.
+      // including the whole change. This cannot be done if the newline is at
+      // the end of the source whitespace though, as this would create a split
+      // where the first part is the same and the second part is empty,
+      // resulting in an infinite loop/stack overflow.
       //
       // Without this, functionality like VS Code's "format modified lines"
       // (which uses Git status to know which lines are edited) may appear to
@@ -188,7 +191,8 @@
       if (unformattedStart < rangeStart.result &&
           unformattedEnd > rangeStart.result &&
           unformattedWhitespace.contains('\n') &&
-          formattedWhitespace.contains('\n')) {
+          formattedWhitespace.contains('\n') &&
+          !unformattedWhitespace.endsWith('\n')) {
         // Find the offsets of the character after the last newlines.
         final unformattedOffset = unformattedWhitespace.lastIndexOf('\n') + 1;
         final formattedOffset = formattedWhitespace.lastIndexOf('\n') + 1;
diff --git a/pkg/analysis_server/test/lsp/format_test.dart b/pkg/analysis_server/test/lsp/format_test.dart
index 61bac2e..7ffff8a 100644
--- a/pkg/analysis_server/test/lsp/format_test.dart
+++ b/pkg/analysis_server/test/lsp/format_test.dart
@@ -295,6 +295,26 @@
     await expectRangeFormattedContents(mainFileUri, contents, expected);
   }
 
+  Future<void> test_formatRange_trailingNewline_47702() async {
+    // Check we complete when a formatted block ends with a newline.
+    // https://github.com/dart-lang/sdk/issues/47702
+    const contents = '''
+int a;
+[[
+    int b;
+]]
+''';
+    final expected = '''
+int a;
+
+int b;
+
+''';
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(contents));
+    await expectRangeFormattedContents(mainFileUri, contents, expected);
+  }
+
   Future<void> test_invalidSyntax() async {
     const contents = '''void f(((( {
   print('test');