diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
index 99e7ba172924..89413dc860cc 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
@@ -77,7 +77,6 @@ void ReportErrorFor(SyntaxTrivia trivia)
}
}
- // The result should be ordered by source location, RemoveDirectivesFromFile depends on that.
return builder.ToImmutable();
}
@@ -112,7 +111,7 @@ public static void FindLeadingDirectives(
{
TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia);
- var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var info = new CSharpDirective.ParseInfo
{
SourceFile = sourceFile,
@@ -134,7 +133,7 @@ public static void FindLeadingDirectives(
var value = parts.Length > 1 ? parts[1] : "";
Debug.Assert(!(parts.Length > 2));
- var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var context = new CSharpDirective.ParseContext
{
Info = new()
@@ -183,35 +182,42 @@ static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia
return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End);
}
- static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index)
+ static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
(WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default;
for (int i = index - 1; i >= 0; i--)
{
- if (!Fill(ref result.Leading, triviaList, i)) break;
+ if (!Fill(ref result.Leading, triviaList, i, excludeSpan)) break;
}
for (int i = index + 1; i < triviaList.Count; i++)
{
- if (!Fill(ref result.Trailing, triviaList, i)) break;
+ if (!Fill(ref result.Trailing, triviaList, i, excludeSpan)) break;
}
return result;
- static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index)
+ static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
var trivia = triviaList[index];
+
+ var length = trivia.FullSpan.Length - (trivia.FullSpan.Intersection(excludeSpan)?.Length ?? 0);
+
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
{
- info.LineBreaks += 1;
- info.TotalLength += trivia.FullSpan.Length;
+ if (length != 0)
+ {
+ info.BlankLineLength += info.RestLength + length;
+ info.RestLength = 0;
+ }
+
return true;
}
if (trivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
- info.TotalLength += trivia.FullSpan.Length;
+ info.RestLength += length;
return true;
}
@@ -256,8 +262,15 @@ internal static partial class Patterns
internal struct WhiteSpaceInfo
{
- public int LineBreaks;
- public int TotalLength;
+ ///
+ /// Size of whitespace that consists of only blank lines (i.e., lines that contain only whitespace).
+ ///
+ public int BlankLineLength;
+
+ ///
+ /// Size of the remaining whitespace on a not-entirely-blank line.
+ ///
+ public int RestLength;
}
///
@@ -271,11 +284,20 @@ internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info)
public readonly struct ParseInfo
{
public required SourceFile SourceFile { get; init; }
+
///
/// Span of the full line including the trailing line break.
///
public required TextSpan Span { get; init; }
+
+ ///
+ /// Additional leading whitespace not included in .
+ ///
public required WhiteSpaceInfo LeadingWhiteSpace { get; init; }
+
+ ///
+ /// Additional trailing whitespace not included in .
+ ///
public required WhiteSpaceInfo TrailingWhiteSpace { get; init; }
}
diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
index df17c8e67fc1..8beab97ae92c 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt
@@ -114,8 +114,8 @@ Microsoft.DotNet.FileBasedPrograms.SourceFile.SourceFile(string! Path, Microsoft
Microsoft.DotNet.FileBasedPrograms.SourceFile.Text.get -> Microsoft.CodeAnalysis.Text.SourceText!
Microsoft.DotNet.FileBasedPrograms.SourceFile.Text.init -> void
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo
-Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.LineBreaks -> int
-Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.TotalLength -> int
+Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.BlankLineLength -> int
+Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.RestLength -> int
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.WhiteSpaceInfo() -> void
Microsoft.DotNet.ProjectTools.ProjectLocator
override abstract Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ToString() -> string!
diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
index 4cf768909127..9199c78c3684 100644
--- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
+++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
@@ -67,7 +67,7 @@ public override int Execute()
}
else
{
- VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, builder.EntryPointSourceFile, targetFile);
+ VirtualProjectBuildingCommand.RemoveDirectivesFromFile(builder.EntryPointSourceFile, targetFile);
}
// Create project file.
@@ -112,7 +112,7 @@ public override int Execute()
else
{
var sourceFile = SourceFile.Load(item.FullPath);
- VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, sourceFile, targetItemFullPath);
+ VirtualProjectBuildingCommand.RemoveDirectivesFromFile(sourceFile, targetItemFullPath);
}
}
else
diff --git a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
index 764aeec88f95..cb7bf28db9ef 100644
--- a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
+++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
@@ -230,27 +230,37 @@ public void Remove(CSharpDirective directive)
{
var span = directive.Info.Span;
var start = span.Start;
- var length = span.Length + DetermineTrailingLengthToRemove(directive);
+ var length = span.Length;
+
+ DetermineWhiteSpaceToRemove(directive, out int leadingLength, out int trailingLength);
+ start -= leadingLength;
+ length += trailingLength;
+
SourceFile = SourceFile with { Text = SourceFile.Text.Replace(start: start, length: length, newText: string.Empty) };
}
- private static int DetermineTrailingLengthToRemove(CSharpDirective directive)
+ private static void DetermineWhiteSpaceToRemove(CSharpDirective directive, out int leadingLength, out int trailingLength)
{
- // If there are blank lines both before and after the directive, remove the trailing white space.
- if (directive.Info.LeadingWhiteSpace.LineBreaks > 0 && directive.Info.TrailingWhiteSpace.LineBreaks > 0)
+ // If there are blank lines both before and after the directive, remove the trailing blank lines.
+ if (directive.Info.LeadingWhiteSpace.BlankLineLength > 0 && directive.Info.TrailingWhiteSpace.BlankLineLength > 0)
{
- return directive.Info.TrailingWhiteSpace.TotalLength;
+ leadingLength = 0;
+ trailingLength = directive.Info.TrailingWhiteSpace.BlankLineLength;
+ return;
}
// If the directive (including leading white space) starts at the beginning of the file,
- // remove both the leading and trailing white space.
- var startBeforeWhiteSpace = directive.Info.Span.Start - directive.Info.LeadingWhiteSpace.TotalLength;
+ // remove both the leading and trailing blank lines.
+ var startBeforeWhiteSpace = directive.Info.Span.Start - directive.Info.LeadingWhiteSpace.BlankLineLength;
if (startBeforeWhiteSpace == 0)
{
- return directive.Info.LeadingWhiteSpace.TotalLength + directive.Info.TrailingWhiteSpace.TotalLength;
+ leadingLength = directive.Info.LeadingWhiteSpace.BlankLineLength;
+ trailingLength = directive.Info.TrailingWhiteSpace.BlankLineLength;
+ return;
}
Debug.Assert(startBeforeWhiteSpace > 0);
- return 0;
+ leadingLength = 0;
+ trailingLength = 0;
}
}
diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
index 3cbd7b9bd1e8..0011c2604535 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -1180,6 +1180,24 @@ public static void CreateTempSubdirectory(string path)
throw new GracefulException(
$"{new SourceFile(path, text).GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}",
innerException);
+
+ public static SourceFile RemoveDirectivesFromFile(SourceFile sourceFile)
+ {
+ var editor = FileBasedAppSourceEditor.Load(sourceFile);
+
+ while (editor.Directives is [{ } directive, ..])
+ {
+ editor.Remove(directive);
+ }
+
+ return editor.SourceFile;
+ }
+
+ public static void RemoveDirectivesFromFile(SourceFile sourceFile, string targetFilePath)
+ {
+ var modifiedFile = RemoveDirectivesFromFile(sourceFile);
+ (modifiedFile with { Path = targetFilePath }).Save();
+ }
}
internal sealed class RunFileBuildCacheEntry
diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs
index 0245bef68abc..0b5a9dcdee1e 100644
--- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs
+++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs
@@ -757,38 +757,4 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s
}
}
}
-
- internal static SourceFile RemoveDirectivesFromFile(ImmutableArray directives, SourceFile sourceFile)
- {
- if (directives.Length == 0)
- {
- return sourceFile;
- }
-
-#if DEBUG
- var filteredDirectives = directives.Where(d => d.Info.SourceFile.Path == sourceFile.Path);
- Debug.Assert(
- filteredDirectives.OrderBy(static d => d.Info.Span.Start).SequenceEqual(filteredDirectives),
- "Directives should be ordered by source location.");
-#endif
-
- var text = sourceFile.Text;
-
- for (int i = directives.Length - 1; i >= 0; i--)
- {
- var directive = directives[i];
- if (directive.Info.SourceFile.Path == sourceFile.Path)
- {
- text = text.Replace(directive.Info.Span, string.Empty);
- }
- }
-
- return sourceFile with { Text = text };
- }
-
- internal static void RemoveDirectivesFromFile(ImmutableArray directives, SourceFile sourceFile, string targetFilePath)
- {
- var modifiedFile = RemoveDirectivesFromFile(directives, sourceFile);
- (modifiedFile with { Path = targetFilePath }).Save();
- }
}
diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
index b379f8eee8bb..adcb13515a90 100644
--- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
+++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
@@ -1645,7 +1645,6 @@ public void Directives_BlankLines()
""",
expectedProject: expectedProject,
expectedCSharp: """
-
Console.WriteLine();
""");
@@ -1968,7 +1967,7 @@ private static void Convert(
actualProject = projectWriter.ToString();
- var convertedFile = VirtualProjectBuilder.RemoveDirectivesFromFile(directives, builder.EntryPointSourceFile);
+ var convertedFile = VirtualProjectBuildingCommand.RemoveDirectivesFromFile(builder.EntryPointSourceFile);
actualCSharp = convertedFile.Text != builder.EntryPointSourceFile.Text ? convertedFile.Text.ToString() : null;
}
diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
index 191276f6a03a..4b1ffa3f73dc 100644
--- a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
@@ -109,6 +109,51 @@ public void PreExistingWhiteSpace()
"""));
}
+ [Fact]
+ public void LeadingWhiteSpace()
+ {
+ Verify(
+ """
+
+ #:package MyPackage@1.0.0
+ Console.WriteLine();
+ """,
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+
+ Console.WriteLine();
+ """));
+
+ Verify(
+ """
+ #:package MyPackage@1.0.0
+ Console.WriteLine();
+ """,
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void WhiteSpaceOutsideLines()
+ {
+ Verify(
+ $"""
+ // trailing{" "}
+
+ #:package MyPackage@1.0.0
+
+ // leading
+ """,
+ (static editor => editor.Remove(editor.Directives.Single()),
+ $"""
+ // trailing{" "}
+
+ // leading
+ """));
+ }
+
[Fact]
public void Comments()
{