diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index 9ae123a0a2db1c39a2ad49eb5231140713115ab6..b66f0510cc26e3c3d297e48d40e758bd3ba48687 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -17,7 +17,7 @@ internal static class Extensions { public static Uri GetURI(this Document document) { - return new Uri(document.FilePath); + return ProtocolConversions.GetUriFromFilePath(document.FilePath); } public static Document GetDocumentFromURI(this Solution solution, Uri fileName) diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 259cfbf0562c01aa86bdf89f9a6c82b9f905eb57..eb98390496ba5249856d576e1161be3b858a3f2c 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.DocumentHighlighting; @@ -59,6 +60,22 @@ internal static class ProtocolConversions { WellKnownTags.NuGet, LSP.CompletionItemKind.Text } }; + public static Uri GetUriFromFilePath(string filePath) + { + if (filePath is null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + // Remove preceding slash if we're on Window as it's an invalid URI. + if (filePath.StartsWith("/") && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + filePath = filePath.Substring(1); + } + + return new Uri(filePath, UriKind.Absolute); + } + public static LSP.TextDocumentPositionParams PositionToTextDocumentPositionParams(int position, SourceText text, Document document) { return new LSP.TextDocumentPositionParams() diff --git a/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs b/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs index 2435f44025778e02e92af27028a793f9a8256b7c..42865230347e65324839dd7f324c9a962d50ebdd 100644 --- a/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs +++ b/src/VisualStudio/Core/Def/Implementation/LanguageClient/InProcLanguageServer.cs @@ -5,6 +5,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -13,6 +14,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -222,31 +224,57 @@ private async void DiagnosticService_DiagnosticsUpdated(object sender, Diagnosti protected virtual async Task PublishDiagnosticsAsync(Document document) { - (var uri, var diagnostics) = await GetDiagnosticsAsync(document, CancellationToken.None).ConfigureAwait(false); - var publishDiagnosticsParams = new PublishDiagnosticParams { Diagnostics = diagnostics, Uri = uri }; - await _jsonRpc.NotifyWithParameterObjectAsync(Methods.TextDocumentPublishDiagnosticsName, publishDiagnosticsParams).ConfigureAwait(false); + var fileUriToDiagnostics = await GetDiagnosticsAsync(document, CancellationToken.None).ConfigureAwait(false); + + var publishTasks = fileUriToDiagnostics.Keys.Select(async fileUri => + { + var publishDiagnosticsParams = new PublishDiagnosticParams { Diagnostics = fileUriToDiagnostics[fileUri], Uri = fileUri }; + await _jsonRpc.NotifyWithParameterObjectAsync(Methods.TextDocumentPublishDiagnosticsName, publishDiagnosticsParams).ConfigureAwait(false); + }); + + await Task.WhenAll(publishTasks).ConfigureAwait(false); } - private async Task<(Uri documentUri, LanguageServer.Protocol.Diagnostic[] diagnostics)> GetDiagnosticsAsync(Document document, CancellationToken cancellationToken) + private async Task> GetDiagnosticsAsync(Document document, CancellationToken cancellationToken) { var diagnostics = _diagnosticService.GetDiagnostics(document.Project.Solution.Workspace, document.Project.Id, document.Id, null, false, cancellationToken); var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - // If there is a mapped file path, use that instead of the document file path for the diagnostics. - var fileUri = diagnostics.FirstOrDefault(d => !string.IsNullOrEmpty(d.DataLocation?.MappedFilePath))?.DataLocation?.MappedFilePath ?? document.FilePath; + // Razor documents can import other razor documents + // https://docs.microsoft.com/en-us/aspnet/core/mvc/views/layout?view=aspnetcore-3.1#importing-shared-directives + // https://docs.microsoft.com/en-us/aspnet/core/blazor/layouts?view=aspnetcore-3.1#centralized-layout-selection + // The imported files contents are added to the content of the generated C# file, so we get diagnostics + // for both the c# contents in the original razor document and for any of the content in any of the imported files when we query diagnostics for the generated C# file. + // These diagnostics will be reported with DiagnosticDataLocation.OriginalFilePath = generated C# file, and DiagnosticDataLocation.MappedFilePath = imported razor file. + // This means that in general we could have diagnostics produced by one generated file that map to many different actual razor files. + // We can't filter them out as we don't know which razor file(s) the underlying generated C# document actually maps to, and which are just imported. + // So we publish them all and let them get de-duped. + var fileUriToDiagnostics = diagnostics.GroupBy(diagnostic => GetDiagnosticUri(document, diagnostic)).ToDictionary( + group => group.Key, + group => group.Select(diagnostic => ConvertToLspDiagnostic(diagnostic, text)).ToArray()); + return fileUriToDiagnostics; + + static Uri GetDiagnosticUri(Document document, DiagnosticData diagnosticData) + { + var filePath = diagnosticData.DataLocation?.MappedFilePath ?? document.FilePath; + return ProtocolConversions.GetUriFromFilePath(filePath); + } - return (new Uri(fileUri), diagnostics.Select(diagnostic => new LanguageServer.Protocol.Diagnostic + static LanguageServer.Protocol.Diagnostic ConvertToLspDiagnostic(DiagnosticData diagnosticData, SourceText text) { - Code = diagnostic.Id, - Message = diagnostic.Message, - Severity = ProtocolConversions.DiagnosticSeverityToLspDiagnositcSeverity(diagnostic.Severity), - Range = GetDiagnosticRange(diagnostic.DataLocation, text), - // Only the unnecessary diagnostic tag is currently supported via LSP. - Tags = diagnostic.CustomTags.Contains("Unnecessary") ? new DiagnosticTag[] { DiagnosticTag.Unnecessary } : Array.Empty() - }).ToArray()); + return new LanguageServer.Protocol.Diagnostic + { + Code = diagnosticData.Id, + Message = diagnosticData.Message, + Severity = ProtocolConversions.DiagnosticSeverityToLspDiagnositcSeverity(diagnosticData.Severity), + Range = GetDiagnosticRange(diagnosticData.DataLocation, text), + // Only the unnecessary diagnostic tag is currently supported via LSP. + Tags = diagnosticData.CustomTags.Contains("Unnecessary") ? new DiagnosticTag[] { DiagnosticTag.Unnecessary } : Array.Empty() + }; + } } - private LanguageServer.Protocol.Range? GetDiagnosticRange(DiagnosticDataLocation? diagnosticDataLocation, SourceText text) + private static LanguageServer.Protocol.Range? GetDiagnosticRange(DiagnosticDataLocation? diagnosticDataLocation, SourceText text) { (var startLine, var endLine) = DiagnosticData.GetLinePositions(diagnosticDataLocation, text, useMapped: true);