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() {