[DAS] Fixes quotes conversion to work the same everywhere

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

Change-Id: Id249ff553cd500c827894a5a94a280f49f86a132
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412321
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Auto-Submit: Felipe Morschel <git@fmorschel.dev>
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/convert_quotes.dart b/pkg/analysis_server/lib/src/services/correction/dart/convert_quotes.dart
index 12d5c29..99ae233 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/convert_quotes.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/convert_quotes.dart
@@ -6,7 +6,6 @@
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
 import 'package:analyzer/dart/ast/ast.dart';
-import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/source/source_range.dart';
 import 'package:analyzer_plugin/utilities/assist/assist.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
@@ -14,7 +13,7 @@
 
 class ConvertQuotes extends _ConvertQuotes {
   @override
-  late bool _fromDouble;
+  late bool _fromSingle;
 
   ConvertQuotes({required super.context});
 
@@ -32,17 +31,13 @@
   Future<void> compute(ChangeBuilder builder) async {
     var node = this.node;
     if (node is SimpleStringLiteral) {
-      _fromDouble = !node.isSingleQuoted;
-      await _simpleStringLiteral(builder, node, addBackslash: false);
-      await _removeBackslash(builder, node.literal);
+      _fromSingle = node.isSingleQuoted;
     } else if (node is StringInterpolation) {
-      _fromDouble = !node.isSingleQuoted;
-      await _stringInterpolation(builder, node);
-
-      for (var child in node.childEntities.whereType<InterpolationString>()) {
-        await _removeBackslash(builder, child.contents);
-      }
+      _fromSingle = node.isSingleQuoted;
+    } else if (node case InterpolationString(:StringInterpolation parent)) {
+      _fromSingle = parent.isSingleQuoted;
     }
+    await super.compute(builder);
   }
 }
 
@@ -63,7 +58,7 @@
   FixKind get multiFixKind => DartFixKind.CONVERT_TO_DOUBLE_QUOTED_STRING_MULTI;
 
   @override
-  bool get _fromDouble => false;
+  bool get _fromSingle => true;
 }
 
 class ConvertToSingleQuotes extends _ConvertQuotes {
@@ -83,116 +78,282 @@
   FixKind get multiFixKind => DartFixKind.CONVERT_TO_SINGLE_QUOTED_STRING_MULTI;
 
   @override
-  bool get _fromDouble => true;
+  bool get _fromSingle => false;
 }
 
 abstract class _ConvertQuotes extends ResolvedCorrectionProducer {
+  static const _backslash = 0x5C;
+  static const _dollar = 0x24;
+
   _ConvertQuotes({required super.context});
 
-  /// Return `true` if this producer is converting from double quotes to single
-  /// quotes, or `false` if it's converting from single quotes to double quotes.
-  bool get _fromDouble;
+  /// Return `true` if this producer is converting from single quotes to double
+  /// quotes, or `false` if it's converting from double quotes to single quotes.
+  bool get _fromSingle;
+
+  _QuotePair get _quotes =>
+      _fromSingle ? _QuotePair.toDouble : _QuotePair.toSingle;
 
   @override
   Future<void> compute(ChangeBuilder builder) async {
     var node = this.node;
+    StringInterpolation? interpolation;
     if (node is SimpleStringLiteral) {
       await _simpleStringLiteral(builder, node);
     } else if (node is StringInterpolation) {
-      await _stringInterpolation(builder, node);
-    } else if (node is InterpolationString) {
-      await _stringInterpolation(builder, node.parent as StringInterpolation);
+      interpolation = node;
+    } else if (node case InterpolationString(:StringInterpolation parent)) {
+      interpolation = parent;
     }
-    await _removeBackslash(builder, token);
+    await _stringInterpolation(builder, interpolation);
   }
 
-  Future<void> _addBackslash(ChangeBuilder builder, Token token) async {
-    var quote = _fromDouble ? "'" : '"';
-    var text = utils.getText(token.offset, token.length);
-    for (var i = 1; i + 1 < text.length; i++) {
-      if ((text[i + 1] == quote) && (text[i] != r'\')) {
-        await builder.addDartFileEdit(file, (builder) {
-          builder.addSimpleInsertion(token.offset + 1 + i, r'\');
-        });
+  bool _canKeepAsRaw(String text) {
+    var newQuoteChar = _quotes.newQuoteString;
+
+    // If the string ends with the new quote, we would have four consecutive
+    // equal quotes, which would close the string and open a new one.
+    if (text.endsWith(newQuoteChar)) {
+      return false;
+    }
+
+    // If the string contains three consecutive new quotes (or more) we would
+    // close the string at that position so this would be invalid.
+    return !text.contains(newQuoteChar * 3);
+  }
+
+  Future<void> _deleteAtOffset(ChangeBuilder builder, int offset) async {
+    await builder.addDartFileEdit(file, (builder) {
+      builder.addDeletion(SourceRange(offset, 1));
+    });
+  }
+
+  Future<void> _escapeBackslashesAndDollars(
+    ChangeBuilder builder,
+    String text,
+    int offset,
+  ) async {
+    for (var i = 0; i < text.length; i++) {
+      var char = text.codeUnitAt(i);
+      if (char == _backslash) {
+        await _insertBackslashAt(builder, offset + i);
+      } else if (char == _dollar) {
+        await _insertBackslashAt(builder, offset + i);
       }
     }
   }
 
-  Future<void> _removeBackslash(ChangeBuilder builder, Token token) async {
-    var quote = _fromDouble ? '"' : "'";
-    var text = utils.getText(token.offset, token.length);
-    for (var i = 0; i + 1 < text.length; i++) {
-      if (text[i] == r'\' && text[i + 1] == quote) {
-        await builder.addDartFileEdit(file, (builder) {
-          builder.addDeletion(SourceRange(token.offset + i, 1));
-        });
-        i++;
+  Future<void> _fixBackslashesForQuotes(
+    ChangeBuilder builder,
+    String text,
+    int offset, {
+    required bool isMultiline,
+    required bool containsStringEnd,
+  }) async {
+    var _QuotePair(:newQuote, :oppositeQuote) = _quotes;
+    var isEscaping = false;
+    var quoteCount = 0;
+
+    for (var i = 0; i < text.length; i++) {
+      var char = text.codeUnitAt(i);
+      if (char == newQuote) {
+        // If we're not escaping, add a backslash before the quote.
+        if (!isEscaping) {
+          if (isMultiline) {
+            quoteCount++;
+            // If we have a triple quote equal to the new quote, we need to
+            // escape the third so it doesn't close the string.
+            if (quoteCount == 3) {
+              await _insertBackslashAt(builder, offset + i);
+              // This quote counts as the first of a new triple quote.
+              quoteCount = 0;
+            } else if (containsStringEnd && (i + 1) == text.length) {
+              // At the end of the multiline string we must always escape the
+              // quote so that our multiline closing works as expected.
+              await _insertBackslashAt(builder, offset + i);
+            }
+          } else {
+            await _insertBackslashAt(builder, offset + i);
+          }
+        } else {
+          // Here we say that we're not escaping anymore, because this is a
+          // quote, not a backslash.
+          isEscaping = false;
+        }
+      } else {
+        quoteCount = 0;
+        if (char == oppositeQuote) {
+          // If we're escaping, remove the backslash before the opposite quote.
+          if (isEscaping) {
+            await _deleteAtOffset(builder, offset + i - 1);
+            // Here we say that we're not escaping anymore, because this is a
+            // quote, not a backslash.
+            isEscaping = false;
+          }
+        }
+        // Swap the escaping state because we found a backslash.
+        isEscaping = (char == _backslash) && !isEscaping;
       }
     }
   }
 
+  Future<void> _insertBackslashAt(ChangeBuilder builder, int offset) async {
+    await builder.addDartFileEdit(file, (builder) {
+      builder.addSimpleInsertion(offset, r'\');
+    });
+  }
+
+  String _newQuote(SingleStringLiteral node) {
+    return node.isMultiline
+        ? _quotes.newQuoteMultilineString
+        : _quotes.newQuoteString;
+  }
+
+  Future<void> _replaceQuotes(
+    ChangeBuilder builder,
+    int offset,
+    int end,
+    String newQuote, {
+    required bool wasRaw,
+    required bool isRaw,
+  }) async {
+    var endQuoteLength = newQuote.length;
+    var startQuoteLength = endQuoteLength;
+    var startQuoteOffset = offset;
+    if (wasRaw) {
+      if (isRaw) {
+        startQuoteOffset += 1;
+      } else {
+        startQuoteLength += 1;
+      }
+    }
+    await builder.addDartFileEdit(file, (builder) {
+      builder.addSimpleReplacement(
+        SourceRange(startQuoteOffset, startQuoteLength),
+        newQuote,
+      );
+      builder.addSimpleReplacement(
+        SourceRange(end - endQuoteLength, endQuoteLength),
+        newQuote,
+      );
+    });
+  }
+
   Future<void> _simpleStringLiteral(
     ChangeBuilder builder,
-    SimpleStringLiteral node, {
-    bool addBackslash = true,
-  }) async {
-    if (_fromDouble ? !node.isSingleQuoted : node.isSingleQuoted) {
-      var newQuote =
-          node.isMultiline
-              ? (_fromDouble ? "'''" : '"""')
-              : (_fromDouble ? "'" : '"');
-      var quoteLength = node.isMultiline ? 3 : 1;
-      var token = node.literal;
+    SimpleStringLiteral node,
+  ) async {
+    if (_fromSingle != node.isSingleQuoted) {
+      return;
+    }
+    var token = node.literal;
+    if (token.isSynthetic) {
+      return;
+    }
 
-      if (addBackslash) {
-        await _addBackslash(builder, token);
-      }
-
-      if (!token.isSynthetic) {
-        await builder.addDartFileEdit(file, (builder) {
-          builder.addSimpleReplacement(
-            SourceRange(node.offset + (node.isRaw ? 1 : 0), quoteLength),
-            newQuote,
-          );
-          builder.addSimpleReplacement(
-            SourceRange(node.end - quoteLength, quoteLength),
-            newQuote,
-          );
-        });
+    var isRaw = node.isRaw;
+    var endQuoteLength = node.isMultiline ? 3 : 1;
+    var startQuoteLength = endQuoteLength;
+    var offset = token.offset + startQuoteLength;
+    var text = utils.getText(
+      offset,
+      token.length - (startQuoteLength + endQuoteLength),
+    );
+    if (isRaw) {
+      offset++;
+      text = text.substring(1);
+      isRaw = _canKeepAsRaw(text);
+      // Removes obsolete escape for opposite quotes.
+      if (!isRaw) {
+        await _escapeBackslashesAndDollars(builder, text, offset);
       }
     }
+
+    await _fixBackslashesForQuotes(
+      builder,
+      text,
+      offset,
+      isMultiline: node.isMultiline,
+      containsStringEnd: true,
+    );
+
+    await _replaceQuotes(
+      builder,
+      node.offset,
+      node.end,
+      _newQuote(node),
+      isRaw: isRaw,
+      wasRaw: node.isRaw,
+    );
   }
 
   Future<void> _stringInterpolation(
     ChangeBuilder builder,
-    StringInterpolation node,
+    StringInterpolation? node,
   ) async {
-    if (_fromDouble ? !node.isSingleQuoted : node.isSingleQuoted) {
-      var newQuote =
-          node.isMultiline
-              ? (_fromDouble ? "'''" : '"""')
-              : (_fromDouble ? "'" : '"');
-      var quoteLength = node.isMultiline ? 3 : 1;
-      var elements = node.elements;
-      for (var i = 0; i < elements.length; i++) {
-        var element = elements[i];
-        if (element is InterpolationString) {
-          var token = element.contents;
-          if (token.isSynthetic || token.lexeme.contains(newQuote)) {
-            return;
-          }
-        }
-      }
-      await builder.addDartFileEdit(file, (builder) {
-        builder.addSimpleReplacement(
-          SourceRange(node.offset + (node.isRaw ? 1 : 0), quoteLength),
-          newQuote,
-        );
-        builder.addSimpleReplacement(
-          SourceRange(node.end - quoteLength, quoteLength),
-          newQuote,
-        );
-      });
+    if (node == null) {
+      return;
     }
+    if (_fromSingle != node.isSingleQuoted) {
+      return;
+    }
+
+    if (node.lastString.endToken.isSynthetic ||
+        node.firstString.beginToken.isSynthetic) {
+      return;
+    }
+
+    var newQuote = _newQuote(node);
+
+    for (var element in node.elements) {
+      if (element is InterpolationString) {
+        var offset = element.offset;
+        var length = element.length;
+        var containsStringEnd = false;
+        if (element == node.firstString) {
+          offset += newQuote.length;
+        } else if (element == node.lastString) {
+          length -= newQuote.length;
+          containsStringEnd = true;
+        }
+        var text = utils.getText(offset, length);
+        await _fixBackslashesForQuotes(
+          builder,
+          text,
+          offset,
+          isMultiline: node.isMultiline,
+          containsStringEnd: containsStringEnd,
+        );
+      }
+    }
+
+    await _replaceQuotes(
+      builder,
+      node.offset,
+      node.end,
+      newQuote,
+      wasRaw: false,
+      isRaw: false,
+    );
   }
 }
+
+enum _QuotePair {
+  toDouble(_doubleQuote, '"', '"""', _singleQuote),
+  toSingle(_singleQuote, "'", "'''", _doubleQuote);
+
+  static const _doubleQuote = 0x22;
+  static const _singleQuote = 0x27;
+
+  final int newQuote;
+  final String newQuoteString;
+  final String newQuoteMultilineString;
+  final int oppositeQuote;
+
+  const _QuotePair(
+    this.newQuote,
+    this.newQuoteString,
+    this.newQuoteMultilineString,
+    this.oppositeQuote,
+  );
+}
diff --git a/pkg/analysis_server/test/src/services/correction/assist/convert_to_double_quoted_string_test.dart b/pkg/analysis_server/test/src/services/correction/assist/convert_to_double_quoted_string_test.dart
index 80902e1..128c078 100644
--- a/pkg/analysis_server/test/src/services/correction/assist/convert_to_double_quoted_string_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/assist/convert_to_double_quoted_string_test.dart
@@ -19,13 +19,130 @@
   @override
   AssistKind get kind => DartAssistKind.CONVERT_TO_DOUBLE_QUOTED_STRING;
 
+  Future<void> test_interpolation_surroundedByEscapedQuote() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print('a \'$b\'');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print("a '$b'");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print('a \"$b\"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print("a \"$b\"");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2_left() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print('a \"$b"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print("a \"$b\"");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2_right() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print('a "$b\"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print("a \"$b\"");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote3() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(' \\"$b\\"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print(" \\\"$b\\\"");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote4() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(' \\\'$b\\\'');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print(" \\'$b\\'");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote5() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(' \\\\"$b\\\\"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print(" \\\\\"$b\\\\\"");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote6() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(' \\\\\'$b\\\\\'');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print(" \\\\'$b\\\\'");
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByQuotes() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print('a "$b"');
+}
+''');
+    await assertHasAssistAt("'", r'''
+void f(int b) {
+  print("a \"$b\"");
+}
+''');
+  }
+
   Future<void> test_one_backslash() async {
     await resolveTestCode(r'''
 void f() {
   print('a\'b\'c');
 }
 ''');
-    await assertHasAssistAt("'a", r'''
+    await assertHasAssistAt("'", r'''
 void f() {
   print("a'b'c");
 }
@@ -38,7 +155,7 @@
   print('a"b"c');
 }
 ''');
-    await assertHasAssistAt("'a", r'''
+    await assertHasAssistAt("'", r'''
 void f() {
   print("a\"b\"c");
 }
@@ -51,21 +168,21 @@
   print("abc");
 }
 ''');
-    await assertNoAssistAt('"ab');
+    await assertNoAssistAt('"');
   }
 
   Future<void> test_one_interpolation() async {
     await resolveTestCode(r'''
 void f() {
-  var b = 'b';
-  var c = 'c';
+  var b = "b";
+  var c = "c";
   print('a $b-${c} d');
 }
 ''');
-    await assertHasAssistAt(r"'a $b", r'''
+    await assertHasAssistAt(r"'", r'''
 void f() {
-  var b = 'b';
-  var c = 'c';
+  var b = "b";
+  var c = "c";
   print("a $b-${c} d");
 }
 ''');
@@ -87,7 +204,7 @@
   print(r'abc');
 }
 ''');
-    await assertHasAssistAt("'ab", '''
+    await assertHasAssistAt("'", '''
 void f() {
   print(r"abc");
 }
@@ -100,7 +217,7 @@
   print('abc');
 }
 ''');
-    await assertHasAssistAt("'ab", '''
+    await assertHasAssistAt("'", '''
 void f() {
   print("abc");
 }
@@ -117,15 +234,94 @@
     await assertNoAssistAt("'");
   }
 
+  Future<void> test_raw_multiLine_manyQuotes() async {
+    await resolveTestCode("""
+void f() {
+  print(r'''
+""\"""\"""\"""
+''');
+}
+""");
+    await assertHasAssistAt("r'", r'''
+void f() {
+  print("""
+""\"""\"""\"""
+""");
+}
+''');
+  }
+
+  Future<void> test_raw_multiLine_threeQuotes() async {
+    await resolveTestCode("""
+void f() {
+  print(r'''
+""\"''');
+}
+""");
+    await assertHasAssistAt("r'", r'''
+void f() {
+  print("""
+""\"""");
+}
+''');
+  }
+
+  Future<void> test_raw_multiLine_twoQuotes() async {
+    await resolveTestCode(r"""
+void f() {
+  print(r'''
+''\''\'
+""
+''');
+}
+""");
+    await assertHasAssistAt("r'", '''
+void f() {
+  print(r"""
+''\''\'
+""
+""");
+}
+''');
+  }
+
+  Future<void> test_raw_multiLine_twoQuotesAtEnd() async {
+    await resolveTestCode("""
+void f() {
+  print(r'''
+""''');
+}
+""");
+    await assertHasAssistAt("r'", r'''
+void f() {
+  print("""
+"\"""");
+}
+''');
+  }
+
+  Future<void> test_raw_nonEscapedChars() async {
+    await resolveTestCode(r'''
+void f() {
+  print(r'\$"');
+}
+''');
+    await assertHasAssistAt("r'", r'''
+void f() {
+  print("\\\$\"");
+}
+''');
+  }
+
   Future<void> test_three_embeddedTarget() async {
     await resolveTestCode("""
 void f() {
   print('''a""\"c''');
 }
 """);
-    await assertHasAssistAt("'a", r'''
+    await assertHasAssistAt("'", r'''
 void f() {
-  print("""a\"\"\"c""");
+  print("""a""\"c""");
 }
 ''');
   }
@@ -136,21 +332,21 @@
   print("""abc""");
 }
 ''');
-    await assertNoAssistAt('"ab');
+    await assertNoAssistAt('"');
   }
 
   Future<void> test_three_interpolation() async {
     await resolveTestCode(r"""
 void f() {
-  var b = 'b';
-  var c = 'c';
+  var b = "b";
+  var c = "c";
   print('''a $b-${c} d''');
 }
 """);
-    await assertHasAssistAt(r"'a $b", r'''
+    await assertHasAssistAt(r"'", r'''
 void f() {
-  var b = 'b';
-  var c = 'c';
+  var b = "b";
+  var c = "c";
   print("""a $b-${c} d""");
 }
 ''');
@@ -162,7 +358,7 @@
   print(r'''abc''');
 }
 """);
-    await assertHasAssistAt("'ab", '''
+    await assertHasAssistAt("'", '''
 void f() {
   print(r"""abc""");
 }
@@ -175,7 +371,7 @@
   print('''abc''');
 }
 """);
-    await assertHasAssistAt("'ab", '''
+    await assertHasAssistAt("'", '''
 void f() {
   print("""abc""");
 }
diff --git a/pkg/analysis_server/test/src/services/correction/assist/convert_to_single_quoted_string_test.dart b/pkg/analysis_server/test/src/services/correction/assist/convert_to_single_quoted_string_test.dart
index 9572d7a..9fc7738 100644
--- a/pkg/analysis_server/test/src/services/correction/assist/convert_to_single_quoted_string_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/assist/convert_to_single_quoted_string_test.dart
@@ -20,13 +20,130 @@
   @override
   AssistKind get kind => DartAssistKind.CONVERT_TO_SINGLE_QUOTED_STRING;
 
+  Future<void> test_interpolation_surroundedByEscapedQuote() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print("a \'$b\'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print('a \'$b\'');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print("a \"$b\"");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print('a "$b"');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2_left() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print("a \'$b'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print('a \'$b\'');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote2_right() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print("a '$b\'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print('a \'$b\'');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote3() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(" \\'$b\\'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print(' \\\'$b\\\'');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote4() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(" \\\"$b\\\"");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print(' \\"$b\\"');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote5() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(" \\\\'$b\\\\'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print(' \\\\\'$b\\\\\'');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByEscapedQuote6() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print(" \\\\\"$b\\\\\"");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print(' \\\\"$b\\\\"');
+}
+''');
+  }
+
+  Future<void> test_interpolation_surroundedByQuotes() async {
+    await resolveTestCode(r'''
+void f(int b) {
+  print("a '$b'");
+}
+''');
+    await assertHasAssistAt('"', r'''
+void f(int b) {
+  print('a \'$b\'');
+}
+''');
+  }
+
   Future<void> test_one_backslash() async {
     await resolveTestCode(r'''
 void f() {
   print("a\"b\"c");
 }
 ''');
-    await assertHasAssistAt('"a', r"""
+    await assertHasAssistAt('"', r"""
 void f() {
   print('a"b"c');
 }
@@ -39,7 +156,7 @@
   print("a'b'c");
 }
 ''');
-    await assertHasAssistAt('"a', r'''
+    await assertHasAssistAt('"', r'''
 void f() {
   print('a\'b\'c');
 }
@@ -52,7 +169,7 @@
   print('abc');
 }
 ''');
-    await assertNoAssistAt("'ab");
+    await assertNoAssistAt("'");
   }
 
   Future<void> test_one_interpolation() async {
@@ -63,7 +180,7 @@
   print("a $b-${c} d");
 }
 ''');
-    await assertHasAssistAt(r'"a $b', r'''
+    await assertHasAssistAt(r'"', r'''
 void f() {
   var b = 'b';
   var c = 'c';
@@ -88,7 +205,7 @@
   print(r"abc");
 }
 ''');
-    await assertHasAssistAt('"ab', '''
+    await assertHasAssistAt('"', '''
 void f() {
   print(r'abc');
 }
@@ -101,7 +218,7 @@
   print("abc");
 }
 ''');
-    await assertHasAssistAt('"ab', '''
+    await assertHasAssistAt('"', '''
 void f() {
   print('abc');
 }
@@ -129,15 +246,94 @@
     await assertNoAssistAt('"');
   }
 
+  Future<void> test_raw_multiLine_manyQuotes() async {
+    await resolveTestCode('''
+void f() {
+  print(r"""
+''\'''\'''\'''
+""");
+}
+''');
+    await assertHasAssistAt('r"', r"""
+void f() {
+  print('''
+''\'''\'''\'''
+''');
+}
+""");
+  }
+
+  Future<void> test_raw_multiLine_threeQuotes() async {
+    await resolveTestCode('''
+void f() {
+  print(r"""
+''\'""");
+}
+''');
+    await assertHasAssistAt('r"', r"""
+void f() {
+  print('''
+''\'''');
+}
+""");
+  }
+
+  Future<void> test_raw_multiLine_twoQuotes() async {
+    await resolveTestCode(r'''
+void f() {
+  print(r"""
+""\""\"
+''
+""");
+}
+''');
+    await assertHasAssistAt('r"', """
+void f() {
+  print(r'''
+""\""\"
+''
+''');
+}
+""");
+  }
+
+  Future<void> test_raw_multiLine_twoQuotesAtEnd() async {
+    await resolveTestCode('''
+void f() {
+  print(r"""
+''""");
+}
+''');
+    await assertHasAssistAt('r"', r"""
+void f() {
+  print('''
+'\'''');
+}
+""");
+  }
+
+  Future<void> test_raw_nonEscapedChars() async {
+    await resolveTestCode(r"""
+void f() {
+  print(r"\$'");
+}
+""");
+    await assertHasAssistAt('r"', r"""
+void f() {
+  print('\\\$\'');
+}
+""");
+  }
+
   Future<void> test_three_embeddedTarget() async {
     await resolveTestCode('''
 void f() {
   print("""a''\'bc""");
 }
 ''');
-    await assertHasAssistAt('"a', r"""
+    await assertHasAssistAt('"', r"""
 void f() {
-  print('''a\'\'\'bc''');
+  print('''a''\'bc''');
 }
 """);
   }
@@ -148,7 +344,7 @@
   print('''abc''');
 }
 """);
-    await assertNoAssistAt("'ab");
+    await assertNoAssistAt("'");
   }
 
   Future<void> test_three_interpolation() async {
@@ -159,7 +355,7 @@
   print("""a $b-${c} d""");
 }
 ''');
-    await assertHasAssistAt(r'"a $b', r"""
+    await assertHasAssistAt(r'"', r"""
 void f() {
   var b = 'b';
   var c = 'c';
@@ -174,7 +370,7 @@
   print(r"""abc""");
 }
 ''');
-    await assertHasAssistAt('"ab', """
+    await assertHasAssistAt('"', """
 void f() {
   print(r'''abc''');
 }
@@ -187,7 +383,7 @@
   print("""abc""");
 }
 ''');
-    await assertHasAssistAt('"ab', """
+    await assertHasAssistAt('"', """
 void f() {
   print('''abc''');
 }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/convert_to_double_quoted_string_test.dart b/pkg/analysis_server/test/src/services/correction/fix/convert_to_double_quoted_string_test.dart
index b9d6a59..2803077 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/convert_to_double_quoted_string_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/convert_to_double_quoted_string_test.dart
@@ -85,7 +85,8 @@
 ''');
   }
 
-  /// More coverage in the `convert_to_double_quoted_string_test.dart` assist test.
+  /// More coverage in the `convert_to_double_quoted_string_test.dart` assist
+  /// test.
   Future<void> test_one_simple() async {
     await resolveTestCode('''
 void f() {
diff --git a/pkg/analysis_server/test/src/services/correction/fix/convert_to_single_quoted_string_test.dart b/pkg/analysis_server/test/src/services/correction/fix/convert_to_single_quoted_string_test.dart
index 48d6b0c..956261f 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/convert_to_single_quoted_string_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/convert_to_single_quoted_string_test.dart
@@ -85,7 +85,8 @@
 ''');
   }
 
-  /// More coverage in the `convert_to_single_quoted_string_test.dart` assist test.
+  /// More coverage in the `convert_to_single_quoted_string_test.dart` assist
+  /// test.
   Future<void> test_one_simple() async {
     await resolveTestCode('''
 void f() {