| File: RazorFormatter.cs | Web Access |
| Project: Microsoft.CodeAnalysis.Razor.DotNetFormat.csproj (Microsoft.CodeAnalysis.Razor.DotNetFormat) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Immutable; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Remote.Razor.Formatting; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.Formatting; public static class RazorFormatter { // dotnet format calls this formatter sequentially. private static readonly IFormattingLoggerFactory s_formattingLoggerFactory = new FormattingLoggerFactory(); private static readonly FormattingLoggerFactoryAdapter s_loggerFactory = new(formattingLogger: null); private static readonly ProjectHostServicesProvider s_hostServicesProvider = new(hostServices: null); private static readonly ImmutableArray<IFormattingPass> s_documentFormattingPasses = [ new RazorFormattingPass(), new CSharpFormattingPass(s_hostServicesProvider, s_loggerFactory) ]; private static readonly FormattingEngine s_formattingEngine = new(s_loggerFactory); public static async Task<SourceText?> TryFormatAsync( Project project, DocumentId documentId, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(project); ArgumentNullException.ThrowIfNull(documentId); var document = project.GetAdditionalDocument(documentId) ?? throw new ArgumentException("The document must be an additional document in the supplied project.", nameof(documentId)); if (!IsRazorDocument(document)) { return null; } var snapshot = await ProjectBackedFormattingSnapshot.TryCreateAsync(document, cancellationToken).ConfigureAwait(false); if (snapshot is null) { return null; } var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var options = GetFormattingOptions(document, sourceText, cancellationToken); var formattingLogger = s_formattingLoggerFactory.CreateLogger(document.FilePath!, "DotNetFormat"); var context = FormattingContext.Create(snapshot, snapshot.CodeDocument, options, formattingLogger); try { s_loggerFactory.SetFormattingLogger(formattingLogger); s_hostServicesProvider.SetHostServices(project.Solution.Workspace.Services.HostServices); var changes = await s_formattingEngine.FormatDocumentAsync( context, s_documentFormattingPasses, htmlChanges: [], range: null, options, cancellationToken).ConfigureAwait(false); return sourceText.WithChanges(changes); } finally { s_loggerFactory.SetFormattingLogger(null); s_hostServicesProvider.SetHostServices(null); } } private static bool IsRazorDocument(TextDocument document) { ArgumentNullException.ThrowIfNull(document); return document.FilePath is { } filePath && Path.GetExtension(filePath) is var extension && (extension.Equals(".razor", StringComparison.OrdinalIgnoreCase) || extension.Equals(".cshtml", StringComparison.OrdinalIgnoreCase)); } private static FormattingEngineOptions GetFormattingOptions( TextDocument document, SourceText sourceText, CancellationToken cancellationToken) { var csharpOptions = document.GetCSharpSyntaxFormattingOptions(cancellationToken); return new() { InsertSpaces = !csharpOptions.LineFormatting.UseTabs, TabSize = csharpOptions.LineFormatting.TabSize, NewLine = GetNewLine(sourceText), CSharpSyntaxFormattingOptions = csharpOptions, }; } private static string GetNewLine(SourceText sourceText) { // dotnet format has no LSP formatting request from which to take a newline. Preserve the document's dominant // convention, using its first newline to break ties and the environment only for a single-line document. var crlfCount = 0; var lfCount = 0; string? firstNewLine = null; foreach (var line in sourceText.Lines) { var lineBreakLength = line.EndIncludingLineBreak - line.End; if (lineBreakLength == 2) { crlfCount++; firstNewLine ??= "\r\n"; } else if (lineBreakLength == 1) { lfCount++; firstNewLine ??= "\n"; } } return lfCount == crlfCount ? firstNewLine ?? Environment.NewLine : lfCount > crlfCount ? "\n" : "\r\n"; } }