diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.cs index 4123fa3e06e..d6a0ad35e27 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/HtmlDocumentServices/HtmlDocumentSynchronizer.cs @@ -174,6 +174,11 @@ private async Task PublishHtmlDocumentAsync(TextDocument return result; } + catch (OperationCanceledException) + { + _logger.LogDebug($"Not publishing Html text for {document.FilePath} as the request was cancelled."); + return default; + } catch (Exception ex) { _logger.LogError(ex, $"Error publishing Html text for {document.FilePath}. Html document contents will be stale"); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs index 578de57a15e..1d88e41669f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingContentValidationPass.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Text; @@ -29,23 +30,12 @@ public Task IsValidAsync(FormattingContext context, ImmutableArray !char.IsWhiteSpace(c)) ?? false) - { - _logger.LogWarning($"{SR.FormatEdit_at_adds(text.GetLinePositionSpan(change.Span), change.NewText)}"); - } - else if (text.TryGetFirstNonWhitespaceOffset(change.Span, out _)) - { - _logger.LogWarning($"{SR.FormatEdit_at_deletes(text.GetLinePositionSpan(change.Span), text.ToString(change.Span))}"); - } - } + var message = GetLogMessage(changes, text); + _logger.LogError(message); if (DebugAssertsEnabled) { - Debug.Fail("A formatting result was rejected because it was going to change non-whitespace content in the document."); + Debug.Fail(message); } return SpecializedTasks.False; @@ -53,4 +43,24 @@ public Task IsValidAsync(FormattingContext context, ImmutableArray changes, SourceText text) + { + using var _1 = StringBuilderPool.GetPooledObject(out var builder); + builder.AppendLine(SR.Format_operation_changed_nonwhitespace); + + foreach (var change in changes) + { + if (change.NewText?.Any(c => !char.IsWhiteSpace(c)) ?? false) + { + builder.AppendLine(SR.FormatEdit_at_adds(text.GetLinePositionSpan(change.Span), change.NewText)); + } + else if (text.TryGetFirstNonWhitespaceOffset(change.Span, out _)) + { + builder.AppendLine(SR.FormatEdit_at_deletes(text.GetLinePositionSpan(change.Span), text.ToString(change.Span))); + } + } + + return builder.ToString(); + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index 4a4636ea380..81f55425eb4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Text; @@ -37,22 +38,12 @@ public async Task IsValidAsync(FormattingContext context, ImmutableArray IsValidAsync(FormattingContext context, ImmutableArray originalDiagnostics, ImmutableArray changedDiagnostics) + { + using var _ = StringBuilderPool.GetPooledObject(out var builder); + + builder.AppendLine(SR.Format_operation_changed_diagnostics); + builder.AppendLine(SR.Diagnostics_before); + foreach (var diagnostic in originalDiagnostics) + { + builder.AppendLine(diagnostic.ToString()); + } + + builder.AppendLine(SR.Diagnostics_after); + foreach (var diagnostic in changedDiagnostics) + { + builder.AppendLine(diagnostic.ToString()); + } + + return builder.ToString(); + } + private class LocationIgnoringDiagnosticComparer : IEqualityComparer { public static IEqualityComparer Instance = new LocationIgnoringDiagnosticComparer(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Logging/RemoteLoggerFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Logging/RemoteLoggerFactory.cs index 3369d22e479..676e100ab92 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Logging/RemoteLoggerFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Logging/RemoteLoggerFactory.cs @@ -13,13 +13,20 @@ internal sealed class RemoteLoggerFactory : ILoggerFactory { private ILoggerFactory _targetLoggerFactory = EmptyLoggerFactory.Instance; - internal void SetTargetLoggerFactory(ILoggerFactory loggerFactory) + /// + /// Sets the logger factory to use in OOP + /// + /// True if the factory was set + internal bool SetTargetLoggerFactory(ILoggerFactory loggerFactory) { // We only set the target logger factory if the current factory is empty. if (_targetLoggerFactory is EmptyLoggerFactory) { _targetLoggerFactory = loggerFactory; + return true; } + + return false; } public void AddLoggerProvider(ILoggerProvider provider) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs index 0416679d0a2..534ff2a26fa 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs @@ -94,7 +94,7 @@ protected virtual async Task CreateInternalAsync( // Note that this means that the first non-empty ILoggerFactory that we use // will be used for MEF component logging for the lifetime of all services. var remoteLoggerFactory = exportProvider.GetExportedValue(); - remoteLoggerFactory.SetTargetLoggerFactory(targetLoggerFactory); + var didSetLoggerFactory = remoteLoggerFactory.SetTargetLoggerFactory(targetLoggerFactory); // In proc services don't use any service hub infra if (stream is null) @@ -103,6 +103,13 @@ protected virtual async Task CreateInternalAsync( return CreateService(in inProcArgs); } + // At this point, we know we're in a remote scenario where we're on the end of a service hub connection, so we want + // logged errors to be thrown, so they're bubbled up to the client. + if (didSetLoggerFactory) + { + remoteLoggerFactory.AddLoggerProvider(new ThrowingErrorLoggerProvider()); + } + var serverConnection = CreateServerConnection(stream, traceSource); var args = new ServiceArgs(serviceBroker.AssumeNotNull(), exportProvider, targetLoggerFactory, workspaceProvider, serverConnection, brokeredServiceData?.Interceptor); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ThrowingErrorLoggerProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ThrowingErrorLoggerProvider.cs new file mode 100644 index 00000000000..01b0967bcf6 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/ThrowingErrorLoggerProvider.cs @@ -0,0 +1,43 @@ +// 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 Microsoft.CodeAnalysis.Razor.Logging; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +/// +/// An error logger provider that provides a logger that simple throws an exception for any LogError call. +/// +internal class ThrowingErrorLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) + { + return Logger.Instance; + } + + private class Logger : ILogger + { + public static readonly Logger Instance = new(); + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel == LogLevel.Error; + } + + public void Log(LogLevel logLevel, string message, Exception? exception) + { + if (logLevel != LogLevel.Error) + { + return; + } + + if (exception is not null) + { + throw exception; + } + + throw new InvalidOperationException(message); + } + } +}