|
6 | 6 | namespace StyleCop.Analyzers.Helpers |
7 | 7 | { |
8 | 8 | using System.Linq; |
| 9 | + using System.Threading; |
| 10 | + using System.Threading.Tasks; |
9 | 11 | using Microsoft.CodeAnalysis; |
10 | 12 | using Microsoft.CodeAnalysis.CodeActions; |
11 | 13 | using Microsoft.CodeAnalysis.CSharp; |
12 | 14 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
13 | 15 | using Microsoft.CodeAnalysis.Formatting; |
| 16 | + using Microsoft.CodeAnalysis.Options; |
| 17 | + using Microsoft.CodeAnalysis.Text; |
14 | 18 |
|
15 | 19 | internal static class FormattingHelper |
16 | 20 | { |
| 21 | + /// <summary> |
| 22 | + /// Retrieves the appropriate end-of-line trivia for use in code fixes at the specified token location. |
| 23 | + /// </summary> |
| 24 | + /// <remarks><para>This method attempts to preserve the existing line ending style near the specified |
| 25 | + /// token. If no suitable end-of-line trivia is found, a default carriage return and line feed (CRLF) is |
| 26 | + /// returned.</para></remarks> |
| 27 | + /// <param name="token">The syntax token for which to determine the end-of-line trivia.</param> |
| 28 | + /// <param name="options">The options to use for formatting. This value is only used if the document does not already contain line endings.</param> |
| 29 | + /// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param> |
| 30 | + /// <returns>A <see cref="SyntaxTrivia"/> representing the end-of-line trivia suitable for code fixes at the given token |
| 31 | + /// position.</returns> |
| 32 | + public static async Task<SyntaxTrivia> GetEndOfLineForCodeFixAsync(SyntaxToken token, OptionSet options, CancellationToken cancellationToken) |
| 33 | + { |
| 34 | + if (TryGetPrecedingEndOfLineTrivia(token, out var precedingEndOfLine)) |
| 35 | + { |
| 36 | + return precedingEndOfLine; |
| 37 | + } |
| 38 | + |
| 39 | + var text = await token.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); |
| 40 | + return GetEndOfLineForCodeFix(token, text, options); |
| 41 | + } |
| 42 | + |
| 43 | + /// <summary> |
| 44 | + /// Retrieves the appropriate end-of-line trivia for use in code fixes, based on the position of the specified |
| 45 | + /// syntax token and the provided source text. |
| 46 | + /// </summary> |
| 47 | + /// <remarks><para>This method examines the token's position and surrounding lines to select the most |
| 48 | + /// contextually appropriate end-of-line trivia, ensuring consistency with the existing file |
| 49 | + /// formatting.</para></remarks> |
| 50 | + /// <param name="token">The syntax token for which to determine the end-of-line trivia.</param> |
| 51 | + /// <param name="text">The source text containing the token. Used to identify line boundaries and end-of-line characters.</param> |
| 52 | + /// <param name="options">The options to use for formatting. This value is only used if the document does not already contain line endings.</param> |
| 53 | + /// <returns>A <see cref="SyntaxTrivia"/> representing the end-of-line trivia suitable for code fixes. If no specific |
| 54 | + /// trivia is found, returns a default carriage return and line feed trivia.</returns> |
| 55 | + public static SyntaxTrivia GetEndOfLineForCodeFix(SyntaxToken token, SourceText text, OptionSet options) |
| 56 | + { |
| 57 | + if (TryGetPrecedingEndOfLineTrivia(token, out var precedingEndOfLine)) |
| 58 | + { |
| 59 | + return precedingEndOfLine; |
| 60 | + } |
| 61 | + |
| 62 | + var lineNumber = token.GetLine(); |
| 63 | + if (lineNumber >= 0 && lineNumber < text.Lines.Count && GetEndOfLineTriviaForLine(text.Lines[lineNumber]) is { } followingTrivia) |
| 64 | + { |
| 65 | + return followingTrivia; |
| 66 | + } |
| 67 | + |
| 68 | + if (lineNumber > 0 && GetEndOfLineTriviaForLine(text.Lines[lineNumber - 1]) is { } precedingTrivia) |
| 69 | + { |
| 70 | + return precedingTrivia; |
| 71 | + } |
| 72 | + |
| 73 | + return SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp)); |
| 74 | + |
| 75 | + static SyntaxTrivia? GetEndOfLineTriviaForLine(TextLine textLine) |
| 76 | + { |
| 77 | + return (textLine.EndIncludingLineBreak - textLine.End) switch |
| 78 | + { |
| 79 | + 2 => SyntaxFactory.CarriageReturnLineFeed, |
| 80 | + 1 => textLine.Text[textLine.End] switch |
| 81 | + { |
| 82 | + '\n' => SyntaxFactory.LineFeed, |
| 83 | + char c => SyntaxFactory.EndOfLine(c.ToString()), |
| 84 | + }, |
| 85 | + _ => null, |
| 86 | + }; |
| 87 | + } |
| 88 | + } |
| 89 | + |
17 | 90 | public static SyntaxTrivia GetNewLineTrivia(Document document) |
18 | 91 | { |
19 | 92 | return SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp)); |
@@ -152,5 +225,39 @@ private static SyntaxTrivia WithoutFormattingImpl(SyntaxTrivia trivia) |
152 | 225 | { |
153 | 226 | return trivia.WithoutAnnotations(Formatter.Annotation, SyntaxAnnotation.ElasticAnnotation); |
154 | 227 | } |
| 228 | + |
| 229 | + /// <summary> |
| 230 | + /// Returns the closest end of line trivia preceding the <paramref name="token"/>. |
| 231 | + /// This currently only looks immediately before the specified token. |
| 232 | + /// </summary> |
| 233 | + /// <param name="token">The token to process.</param> |
| 234 | + /// <param name="trivia">When this method returns, contains the closest preceding end of line trivia, if found; otherwise, the default value.</param> |
| 235 | + /// <returns><see langword="true"/> if an end of line trivia was found; otherwise, <see langword="false"/>.</returns> |
| 236 | + private static bool TryGetPrecedingEndOfLineTrivia(this SyntaxToken token, out SyntaxTrivia trivia) |
| 237 | + { |
| 238 | + var leadingTrivia = token.LeadingTrivia; |
| 239 | + for (var i = leadingTrivia.Count - 1; i >= 0; i--) |
| 240 | + { |
| 241 | + if (leadingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia)) |
| 242 | + { |
| 243 | + trivia = leadingTrivia[i]; |
| 244 | + return true; |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + var prevToken = token.GetPreviousToken(); |
| 249 | + var prevTrailingTrivia = prevToken.TrailingTrivia; |
| 250 | + for (var i = prevTrailingTrivia.Count - 1; i >= 0; i--) |
| 251 | + { |
| 252 | + if (prevTrailingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia)) |
| 253 | + { |
| 254 | + trivia = prevTrailingTrivia[i]; |
| 255 | + return true; |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + trivia = default; |
| 260 | + return false; |
| 261 | + } |
155 | 262 | } |
156 | 263 | } |
0 commit comments