diff --git a/src/DiffEngine.Tests/InlinePatcherTests.cs b/src/DiffEngine.Tests/InlinePatcherTests.cs
index ece3b054..6c7db593 100644
--- a/src/DiffEngine.Tests/InlinePatcherTests.cs
+++ b/src/DiffEngine.Tests/InlinePatcherTests.cs
@@ -74,6 +74,52 @@ public async Task ASnapshotBeforeARawInterpolatedStringIsStillFound()
const string rawInterpolated =
" var text = $" + q3 + "{Render(" + q4 + "has " + q3 + " inside" + q4 + ")}" + q3 + ";";
+ ///
+ /// A verbatim string that opens on an escaped quote. Measuring the quote run before asking
+ /// whether the literal is verbatim lexed this as a 3-quote raw string, which then ran to the
+ /// end of the file and hid every call after it. There is no verbatim raw form, so a run of
+ /// quotes after @" is content, not a delimiter.
+ ///
+ [Test]
+ public async Task AVerbatimStringOpeningOnAnEscapedQuoteIsSteppedOverWhole()
+ {
+ var source = Method($"{verbatimEscapedQuote}\n await Snapshot(\"old\");");
+ var status = TryApply(source, 6, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out var reason);
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(reason).IsEmpty();
+ await Assert.That(newSource).Contains("Snapshot(\"new\")");
+ // And the literal it stepped over is untouched
+ await Assert.That(newSource).Contains(verbatimEscapedQuote);
+ }
+
+ ///
+ /// The same shape with the call before it, so the scan has to get past the literal rather
+ /// than stop short of it.
+ ///
+ [Test]
+ public async Task ASnapshotBeforeAVerbatimStringOpeningOnAnEscapedQuoteIsStillFound()
+ {
+ var source = Method($" await Snapshot(\"old\");\n{verbatimEscapedQuote}");
+ var status = TryApply(source, 5, InlinePatchMode.Set, "\"old\"", "new", out var newSource, out _);
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(newSource).Contains("Snapshot(\"new\")");
+ await Assert.That(newSource).Contains(verbatimEscapedQuote);
+ }
+
+ // A verbatim string whose first content character is an escaped quote:
+ //
+ // var path = @"""C:\tools\run.exe"" --flag";
+ //
+ // By concatenation for the same reason rawInterpolated is: the quote runs cannot be written
+ // in a literal here without a delimiter wider than the thing being described.
+ const string verbatimEscapedQuote =
+ " var path = @" + q3 + "C:" + slash + "tools" + slash + "run.exe" + q2 + " --flag" + q1 + ";";
+
+ const string q1 = "\"";
+ const string q2 = "\"\"";
+ const string slash = "\\";
const string q3 = "\"\"\"";
const string q4 = "\"\"\"\"";
diff --git a/src/DiffEngine/Inline/CsLanguage.cs b/src/DiffEngine/Inline/CsLanguage.cs
index a74eb9e5..f8756749 100644
--- a/src/DiffEngine/Inline/CsLanguage.cs
+++ b/src/DiffEngine/Inline/CsLanguage.cs
@@ -208,7 +208,12 @@ static bool TrySkipStringLike(string source, ref int index)
}
var quotes = StringLiteral.QuoteRunLength(source, cursor);
- if (quotes >= 3)
+ // Asked before the run is treated as a delimiter, because there is no verbatim raw form:
+ // a run of quotes after @" is an escaped quote and the start of the content. Measuring
+ // alone lexed a verbatim string opening on an escaped quote as a 3-quote raw string,
+ // which then ran to the end of the file and hid every call after it. CsStringLiteral and
+ // FsLanguage already ask this, and for the same reason
+ if (quotes >= 3 && !verbatim)
{
// Raw string: scan to a closing run of >= quotes, stepping over any interpolation
// hole whole. Skipping holes as content read the quotes of a literal inside one -