diff --git a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs
index ac4398faf2..1488fbd1a0 100644
--- a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs
+++ b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs
@@ -223,6 +223,7 @@ public void Enable_WhenCompiledLineDriftsFromEditedFile_AddsDriftWarningAndNextA
string[] snapshotLines = diskSource.Replace("\r\n", "\n").Split('\n');
snapshotLines[requestedLine - 1] = " return 0;";
+ snapshotLines[markerLine - 1] = " return 424242;";
string snapshotSource = string.Join("\n", snapshotLines);
try
@@ -259,6 +260,10 @@ public void Enable_WhenCompiledLineDriftsFromEditedFile_AddsDriftWarningAndNextA
response.ResolvedMethod,
spanResult.Resolution.CompiledMethodStartLine,
spanResult.Resolution.CompiledMethodEndLine);
+ expectedDrift = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ expectedDrift,
+ "return 424242;",
+ snapshotLines);
string expectedWarning = PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.MergeWarnings(
PausePointEnableWarnings.MergeWarnings(
@@ -344,6 +349,145 @@ public void AppendCompiledMethodSpanToDriftWarningOrUnchanged_WhenDriftIsEmpty_R
Assert.That(warning, Is.EqualTo(string.Empty));
}
+ ///
+ /// What: one trimmed compiled-source match appends a single-line candidate to the drift warning.
+ ///
+ [Test]
+ public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenOneLineMatches_AppendsSingleCandidate()
+ {
+ string drift = string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
+ ForwardSlashFile,
+ 17,
+ "return 1;",
+ "return 2;");
+ string[] compiledLines =
+ {
+ "class Sample",
+ " return 2;",
+ " return 1;"
+ };
+
+ string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ drift,
+ " return 2; ",
+ compiledLines);
+
+ Assert.That(
+ warning,
+ Is.EqualTo(
+ drift + " Candidate: the edited line's text appears at line 2 in the last compiled source."));
+ }
+
+ ///
+ /// What: two or three trimmed compiled-source matches append every matching line number.
+ ///
+ [Test]
+ public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenThreeLinesMatch_AppendsAllCandidates()
+ {
+ string drift = string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
+ ForwardSlashFile,
+ 17,
+ "return 1;",
+ "return 2;");
+ string[] compiledLines =
+ {
+ "class Sample",
+ " return 2;",
+ " return 1;",
+ " return 2;",
+ " return 2;"
+ };
+
+ string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ drift,
+ "return 2;",
+ compiledLines);
+
+ Assert.That(
+ warning,
+ Is.EqualTo(
+ drift + " Candidate: the edited line's text appears at lines 2, 4, 5 in the last compiled source."));
+ }
+
+ ///
+ /// What: more than three trimmed compiled-source matches append the first three and a truncation note.
+ ///
+ [Test]
+ public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenMoreThanThreeLinesMatch_CapsAtFirstThree()
+ {
+ string drift = string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
+ ForwardSlashFile,
+ 17,
+ "return 1;",
+ "return 2;");
+ string[] compiledLines =
+ {
+ " return 2;",
+ " return 1;",
+ " return 2;",
+ " return 2;",
+ " return 2;"
+ };
+
+ string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ drift,
+ "return 2;",
+ compiledLines);
+
+ Assert.That(
+ warning,
+ Is.EqualTo(
+ drift + " Candidate: the edited line's text appears at lines 1, 3, 4 (first 3 matches) in the last compiled source."));
+ }
+
+ ///
+ /// What: no compiled-source match leaves the drift warning unchanged.
+ ///
+ [Test]
+ public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenNoLineMatches_LeavesWarningUnchanged()
+ {
+ string drift = string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineMapLineDriftWarningFormat,
+ ForwardSlashFile,
+ 17,
+ "return 1;",
+ "return 2;");
+ string[] compiledLines =
+ {
+ "class Sample",
+ " return 1;"
+ };
+
+ string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ drift,
+ "return 2;",
+ compiledLines);
+
+ Assert.That(warning, Is.EqualTo(drift));
+ }
+
+ ///
+ /// What: an empty drift warning stays empty even when compiled source contains the edited text.
+ ///
+ [Test]
+ public void AppendCandidateCompiledLinesToDriftWarningOrUnchanged_WhenDriftIsEmpty_ReturnsEmpty()
+ {
+ string[] compiledLines =
+ {
+ " return 2;"
+ };
+
+ string warning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ string.Empty,
+ "return 2;",
+ compiledLines);
+
+ Assert.That(warning, Is.EqualTo(string.Empty));
+ }
+
///
/// What: retarget warning interpolates resolved method, requested line, and edited span.
///
diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs
index 206adbe195..6c858ab08a 100644
--- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs
+++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs
@@ -115,6 +115,94 @@ internal static string AppendCompiledMethodSpanToDriftWarningOrUnchanged(
compiledMethodEndLine);
}
+ // Why only after a non-empty drift warning: a candidate list without drift would look
+ // like a second resolution, and empty edited text never produces drift in the first place.
+ internal static string AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ string driftWarning,
+ string editedLineText,
+ IReadOnlyList compiledSourceLines)
+ {
+ if (string.IsNullOrEmpty(driftWarning))
+ {
+ return driftWarning ?? string.Empty;
+ }
+
+ if (string.IsNullOrEmpty(editedLineText) || compiledSourceLines == null)
+ {
+ return driftWarning;
+ }
+
+ string editedTrimmed = editedLineText.Trim();
+ if (editedTrimmed.Length == 0)
+ {
+ return driftWarning;
+ }
+
+ (List matches, bool truncated) = CollectCandidateCompiledLineNumbers(
+ editedTrimmed,
+ compiledSourceLines);
+ if (matches.Count == 0)
+ {
+ return driftWarning;
+ }
+
+ return driftWarning + FormatCandidateCompiledLinesSuffix(matches, truncated);
+ }
+
+ private static (List matches, bool truncated) CollectCandidateCompiledLineNumbers(
+ string editedTrimmed,
+ IReadOnlyList compiledSourceLines)
+ {
+ int matchLimit = SourcePausePointConstants.CompiledLineDriftCandidateMatchLimit;
+ List matches = new List();
+ bool truncated = false;
+ for (int index = 0; index < compiledSourceLines.Count; index++)
+ {
+ string compiledLine = compiledSourceLines[index];
+ if (compiledLine == null)
+ {
+ continue;
+ }
+
+ if (!string.Equals(compiledLine.Trim(), editedTrimmed, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (matches.Count == matchLimit)
+ {
+ truncated = true;
+ break;
+ }
+
+ matches.Add(index + 1);
+ }
+
+ return (matches, truncated);
+ }
+
+ private static string FormatCandidateCompiledLinesSuffix(List matches, bool truncated)
+ {
+ if (matches.Count == 1 && !truncated)
+ {
+ return string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineDriftCandidateSingleFormat,
+ matches[0]);
+ }
+
+ string listed = string.Join(", ", matches);
+ if (truncated)
+ {
+ listed += string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineDriftCandidateTruncatedMatchesSuffixFormat,
+ SourcePausePointConstants.CompiledLineDriftCandidateMatchLimit);
+ }
+
+ return string.Format(
+ SourcePausePointConstants.HotReloadCompiledLineDriftCandidateMultipleFormat,
+ listed);
+ }
+
internal static string BuildRetargetedToHotReloadPatchWarningOrEmpty(
bool retargetedToHotReloadPatch,
string resolvedMethod,
diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs
index 236759afa6..d84cab8686 100644
--- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs
+++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs
@@ -350,12 +350,18 @@ private static PausePointResponse FinishEnableBySourceLocation(
snapshot = UloopPausePointRegistry.GetStatus(id);
}
- string resolvedLineText = ResolveEnableLineText(
- parameters.File,
- resolvedLine,
- resolvedEndLine,
- retargetedToHotReloadPatch,
- hasActiveHotReloadPatches);
+ bool compareCompiledLineDrift = hasActiveHotReloadPatches && !retargetedToHotReloadPatch;
+ string compiledSnapshotSource = compareCompiledLineDrift
+ ? LoadCompiledSnapshotSourceOrEmpty(parameters.File)
+ : string.Empty;
+ // Why snapshot over disk: the editor file may already include unpatched-line drift, so
+ // reading disk at the compiled ResolvedLine shows the wrong statement (FB9 empty/mismatch).
+ // The snapshot read stays single-line because the verified snapshot has no end-line data;
+ // the disk read spans resolvedLine..resolvedEndLine so a rounded-forward multi-line
+ // statement returns its full text.
+ string resolvedLineText = compareCompiledLineDrift
+ ? SourcePausePointSourceLineReader.ReadLineTextFromSource(compiledSnapshotSource, resolvedLine)
+ : PausePointLineTextReader.ReadResolvedLineText(parameters.File, resolvedLine, resolvedEndLine);
UloopPausePointRegistry.SetResolvedLine(id, resolvedLine, resolvedLineText);
PausePointResponse response = PausePointResponse.FromSnapshot(snapshot);
@@ -373,7 +379,6 @@ private static PausePointResponse FinishEnableBySourceLocation(
parameters.Line,
editedMethodStartLine,
editedMethodEndLine));
- bool compareCompiledLineDrift = hasActiveHotReloadPatches && !retargetedToHotReloadPatch;
string compiledLineMapWarning = PausePointEnableWarnings.ChooseCompiledLineMapWarning(
patchedMethodPdbUnavailableWarning,
PausePointEnableWarnings.BuildCompiledLineMapWarningOrEmpty(
@@ -393,6 +398,11 @@ private static PausePointResponse FinishEnableBySourceLocation(
resolvedMethod,
compiledMethodStartLine,
compiledMethodEndLine);
+ string[] compiledSourceLines = SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource);
+ driftWarning = PausePointEnableWarnings.AppendCandidateCompiledLinesToDriftWarningOrUnchanged(
+ driftWarning,
+ editedLineText,
+ compiledSourceLines);
enableWarning = PausePointEnableWarnings.MergeWarnings(enableWarning, driftWarning);
if (driftWarning.Length > 0)
{
@@ -466,27 +476,12 @@ private static void LogPhysicsDispatchDiagnostics(string operation, string id, T
});
}
- // Why snapshot over disk: the editor file may already include unpatched-line drift, so
- // reading disk at the compiled ResolvedLine shows the wrong statement (FB9 empty/mismatch).
- // The snapshot read stays single-line because the verified snapshot has no end-line data;
- // the disk read spans resolvedLine..resolvedEndLine so a rounded-forward multi-line
- // statement returns its full text.
- private static string ResolveEnableLineText(
- string requestedFile,
- int resolvedLine,
- int resolvedEndLine,
- bool retargetedToHotReloadPatch,
- bool hasActiveHotReloadPatches)
+ private static string LoadCompiledSnapshotSourceOrEmpty(string requestedFile)
{
- if (hasActiveHotReloadPatches && !retargetedToHotReloadPatch)
- {
- string normalizedFile = SourcePausePointPathNormalizer.ToForwardSlashes(requestedFile);
- string snapshotSource =
- HotReloadPausePointCoordination.GetVerifiedSnapshotSourceForFile?.Invoke(normalizedFile);
- return SourcePausePointSourceLineReader.ReadLineTextFromSource(snapshotSource, resolvedLine);
- }
-
- return PausePointLineTextReader.ReadResolvedLineText(requestedFile, resolvedLine, resolvedEndLine);
+ string normalizedFile = SourcePausePointPathNormalizer.ToForwardSlashes(requestedFile);
+ string snapshotSource =
+ HotReloadPausePointCoordination.GetVerifiedSnapshotSourceForFile?.Invoke(normalizedFile);
+ return snapshotSource ?? string.Empty;
}
// The derived id must use the originally requested file/line (not the resolved/rounded
diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
index 960bb5d5e7..8db0234110 100644
--- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
+++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
@@ -256,6 +256,23 @@ internal static class SourcePausePointConstants
public const string HotReloadCompiledMethodSpanInLastCompiledSourceFormat =
" In the last compiled source, '{0}' spans lines {1}-{2}.";
+ // Why cap 3: a longer match list turns the enable warning into another line-number puzzle.
+ public const int CompiledLineDriftCandidateMatchLimit = 3;
+
+ // Format: 1-based compiled line number. Why "Candidate": this is a search hit, not a
+ // guarantee that re-enabling there is the intended statement.
+ public const string HotReloadCompiledLineDriftCandidateSingleFormat =
+ " Candidate: the edited line's text appears at line {0} in the last compiled source.";
+
+ // Format: comma-separated 1-based compiled line numbers, with an optional truncation note.
+ public const string HotReloadCompiledLineDriftCandidateMultipleFormat =
+ " Candidate: the edited line's text appears at lines {0} in the last compiled source.";
+
+ // Why format from CompiledLineDriftCandidateMatchLimit: a hard-coded "3" would lie
+ // if the cap changed.
+ public const string HotReloadCompiledLineDriftCandidateTruncatedMatchesSuffixFormat =
+ " (first {0} matches)";
+
public const string NearbyCompiledMethodsPrefix =
" Nearby methods in the last compiled source: ";
diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs
index 4b5e2364ae..2707418d09 100644
--- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs
+++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -34,6 +35,16 @@ public static string ReadLineText(string absoluteFilePath, int startLine, int en
return string.Join(" ", trimmedNonEmptyLines);
}
+ public static string[] SplitSourceLines(string sourceText)
+ {
+ if (string.IsNullOrEmpty(sourceText))
+ {
+ return Array.Empty();
+ }
+
+ return sourceText.Replace("\r\n", "\n").Split('\n');
+ }
+
public static string ReadLineTextFromSource(string sourceText, int lineNumber)
{
if (string.IsNullOrEmpty(sourceText) || lineNumber <= 0)
@@ -41,7 +52,7 @@ public static string ReadLineTextFromSource(string sourceText, int lineNumber)
return string.Empty;
}
- string[] lines = sourceText.Replace("\r\n", "\n").Split('\n');
+ string[] lines = SplitSourceLines(sourceText);
if (lineNumber > lines.Length)
{
return string.Empty;