diff --git a/src/DiffEngine.Tests/FsStringLiteralTests.cs b/src/DiffEngine.Tests/FsStringLiteralTests.cs
index e6841543..d48dced0 100644
--- a/src/DiffEngine.Tests/FsStringLiteralTests.cs
+++ b/src/DiffEngine.Tests/FsStringLiteralTests.cs
@@ -300,12 +300,21 @@ public async Task ParseRejects(string expression)
await Assert.That(parsed).IsFalse();
}
- // The indent is stripped by ordinal prefix, so a content line less indented than the closing
- // delimiter is not something this can read
+ // A triple-quoted literal that is not written to the layout convention. F# has no raw string
+ // form, so this is a perfectly good literal and its value is simply what it says - which is
+ // also what StripLayout returns for the same text, and the two have to agree or an
+ // OriginalValue anchor can never match.
[Test]
- public async Task ParseRejectsMalformedIndent()
+ [Arguments("\"\"\"\n a\n \"\"\"", "\n a\n ")]
+ // The idiomatic hand-written shape: content starting on the opening line.
+ [Arguments("\"\"\"{\n \"a\": 1\n}\"\"\"", "{\n \"a\": 1\n}")]
+ public async Task ParseKeepsContentWithNoLayoutToStrip(string expression, string expected)
{
- var parsed = FsStringLiteral.TryParse("\"\"\"\n a\n \"\"\"", out _);
- await Assert.That(parsed).IsFalse();
+ var parsed = FsStringLiteral.TryParse(expression, out var value);
+ await Assert.That(parsed).IsTrue();
+ await Assert.That(value).IsEqualTo(expected);
+ // Whatever a producer sends as OriginalValue for this literal goes through StripLayout,
+ // so the two readers have to land in the same place
+ await Assert.That(FsStringLiteral.StripLayout(value!)).IsEqualTo(expected);
}
}
diff --git a/src/DiffEngine.Tests/InlinePatcherFsTests.cs b/src/DiffEngine.Tests/InlinePatcherFsTests.cs
index 5726f10a..86350c0b 100644
--- a/src/DiffEngine.Tests/InlinePatcherFsTests.cs
+++ b/src/DiffEngine.Tests/InlinePatcherFsTests.cs
@@ -28,6 +28,34 @@ await Assert.That(newSource).IsEqualTo(
Test(" Verifier.Verify(15).Snapshot(\"new\").ToTask() |> Async.AwaitTask"));
}
+ ///
+ /// A hand-written triple-quoted literal, content starting on the opening line. F# has no raw
+ /// string, so the layout is only a convention and a literal not written to it still holds a
+ /// perfectly good value. Rejecting those made them unpatchable: the anchor an F# producer
+ /// sends is OriginalValue (FS0202 means there is no CallerArgumentExpression to send), and
+ /// what the parser read back had to be able to equal it.
+ ///
+ [Test]
+ public async Task ReplaceHandWrittenTripleQuotedLiteral()
+ {
+ var literal = "\"\"\"{\n \"a\": 1\n}\"\"\"";
+ var source = Test($" Verifier.Verify(15).Snapshot({literal}).ToTask() |> Async.AwaitTask");
+
+ var status = TryApply(
+ source,
+ 5,
+ InlinePatchMode.Set,
+ null,
+ "new",
+ out var newSource,
+ out var reason,
+ originalValue: FsStringLiteral.StripLayout("{\n \"a\": 1\n}"));
+
+ await Assert.That(status).IsEqualTo(PatchStatus.Applied);
+ await Assert.That(reason).IsEmpty();
+ await Assert.That(newSource).IsEqualTo(
+ Test(" Verifier.Verify(15).Snapshot(\"new\").ToTask() |> Async.AwaitTask"));
+ }
// The same shape C# writes: the content indented under the call, with the first line and the
// closing delimiter's indentation there for the reader to take back off
[Test]
diff --git a/src/DiffEngine/Inline/CsStringLiteral.cs b/src/DiffEngine/Inline/CsStringLiteral.cs
index 8d653087..64eb56d6 100644
--- a/src/DiffEngine/Inline/CsStringLiteral.cs
+++ b/src/DiffEngine/Inline/CsStringLiteral.cs
@@ -106,7 +106,7 @@ static bool TryScanLiteral(string text, int start, out string? value, out int en
var quotes = StringLiteral.QuoteRunLength(text, index);
if (quotes >= 3)
{
- return StringLiteral.TryScanMultiLine(text, index, quotes, out value, out end);
+ return StringLiteral.TryScanMultiLine(text, index, quotes, true, out value, out end);
}
if (quotes == 2)
diff --git a/src/DiffEngine/Inline/FsStringLiteral.cs b/src/DiffEngine/Inline/FsStringLiteral.cs
index 1a65e532..d2fc4666 100644
--- a/src/DiffEngine/Inline/FsStringLiteral.cs
+++ b/src/DiffEngine/Inline/FsStringLiteral.cs
@@ -85,9 +85,10 @@ public static string StripLayout(string value) =>
///
/// Parses an F# string literal expression back to the snapshot it holds: triple-quoted
- /// ("""..."""), with the layout taken off, verbatim (@"...") and regular ("..."), which carry
- /// their value as it is. Returns false for interpolated strings, byte strings, concatenations,
- /// or any other expression. Newlines in the returned value are normalized to \n.
+ /// ("""..."""), with the layout taken off when it is written to that convention and
+ /// verbatim when it is not; verbatim (@"...") and regular ("..."), which carry their value as
+ /// it is. Returns false for interpolated strings, byte strings, concatenations, or any other
+ /// expression. Newlines in the returned value are normalized to \n.
///
public static bool TryParse(string expression, [NotNullWhen(true)] out string? value) =>
StringLiteral.TryParse(expression, TryScanLiteral, out value);
@@ -131,7 +132,7 @@ static bool TryScanLiteral(string text, int start, out string? value, out int en
{
// F# reads the closing delimiter as exactly three quotes, so a longer run is content
// it cannot hold and never something this wrote
- return StringLiteral.TryScanMultiLine(text, index, 3, out value, out end);
+ return StringLiteral.TryScanMultiLine(text, index, 3, false, out value, out end);
}
if (quotes == 2)
diff --git a/src/DiffEngine/Inline/StringLiteral.cs b/src/DiffEngine/Inline/StringLiteral.cs
index cf11fc38..b3fbc4cf 100644
--- a/src/DiffEngine/Inline/StringLiteral.cs
+++ b/src/DiffEngine/Inline/StringLiteral.cs
@@ -184,8 +184,24 @@ public static int QuoteRunLength(string text, int index)
///
/// Scans a multi-line literal opening at with a run of
/// , and returns what it holds with the layout taken off.
+ ///
+ /// is what separates the two languages. A C# raw string not
+ /// in the layout shape does not compile, so content that fails the strip is a literal this
+ /// never wrote and rejecting it is right. F#'s triple-quoted literal is plain verbatim text
+ /// and the layout is only a convention, so a hand-written one - content starting on the
+ /// opening line, which is the idiomatic shape - is a perfectly good literal whose value is
+ /// simply what it says. Rejecting those made them unpatchable: TryParse said "not a string
+ /// literal" while StripLayout, which is what a test library runs the value through, returned
+ /// the same text unchanged, so the producer's OriginalValue could never match.
+ ///
///
- public static bool TryScanMultiLine(string text, int start, int quotes, out string? value, out int end)
+ public static bool TryScanMultiLine(
+ string text,
+ int start,
+ int quotes,
+ bool layoutRequired,
+ out string? value,
+ out int end)
{
value = null;
end = start;
@@ -224,7 +240,21 @@ public static bool TryScanMultiLine(string text, int start, int quotes, out stri
return true;
}
- return TryStripLayout(SourceLanguage.NormalizeNewlines(content), out value);
+ var normalized = SourceLanguage.NormalizeNewlines(content);
+ if (TryStripLayout(normalized, out value))
+ {
+ return true;
+ }
+
+ if (layoutRequired)
+ {
+ return false;
+ }
+
+ // Not in the layout shape, so there is no layout to take off and the content is the
+ // value. The same answer StripLayout gives for the same text
+ value = normalized;
+ return true;
}
///