From d3df3e50c08ae7856dbffe777c0a6a104887c95a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 14:30:38 +1100 Subject: [PATCH 1/9] Get todo list descriptors from VS --- .../Hosting/RazorLSPOptions.cs | 10 ++-- .../Settings/ClientSettings.cs | 23 ++++++++- .../LanguageClient/Options/OptionsStorage.cs | 50 ++++++++++++++++++- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs index c8d1a284810..4fae44d9dd7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using Microsoft.CodeAnalysis.Razor.Settings; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; @@ -16,7 +17,8 @@ internal record RazorLSPOptions( bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, - bool CommitElementsWithSpace) + bool CommitElementsWithSpace, + ImmutableArray TaskListDescriptors) { public readonly static RazorLSPOptions Default = new(Formatting: FormattingFlags.All, AutoClosingTags: true, @@ -27,7 +29,8 @@ internal record RazorLSPOptions( AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, - CommitElementsWithSpace: true); + CommitElementsWithSpace: true, + TaskListDescriptors: []); /// /// Initializes the LSP options with the settings from the passed in client settings, and default values for anything @@ -43,7 +46,8 @@ internal static RazorLSPOptions From(ClientSettings settings) settings.AdvancedSettings.AutoInsertAttributeQuotes, settings.AdvancedSettings.ColorBackground, settings.AdvancedSettings.CodeBlockBraceOnNextLine, - settings.AdvancedSettings.CommitElementsWithSpace); + settings.AdvancedSettings.CommitElementsWithSpace, + settings.AdvancedSettings.TaskListDescriptors); private static FormattingFlags GetFormattingFlags(ClientSettings settings) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs index f0e9036a619..7c0649daabc 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using Microsoft.CodeAnalysis.Razor.Logging; namespace Microsoft.CodeAnalysis.Razor.Settings; @@ -32,7 +33,25 @@ internal sealed record ClientSpaceSettings(bool IndentWithTabs, int IndentSize) public int IndentSize { get; } = IndentSize >= 0 ? IndentSize : throw new ArgumentOutOfRangeException(nameof(IndentSize)); } -internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel, bool FormatOnPaste) +internal sealed record ClientAdvancedSettings(bool FormatOnType, + bool AutoClosingTags, + bool AutoInsertAttributeQuotes, + bool ColorBackground, + bool CodeBlockBraceOnNextLine, + bool CommitElementsWithSpace, + SnippetSetting SnippetSetting, + LogLevel LogLevel, + bool FormatOnPaste, + ImmutableArray TaskListDescriptors) { - public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning, FormatOnPaste: true); + public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, + AutoClosingTags: true, + AutoInsertAttributeQuotes: true, + ColorBackground: false, + CodeBlockBraceOnNextLine: false, + CommitElementsWithSpace: true, + SnippetSetting.All, + LogLevel.Warning, + FormatOnPaste: true, + TaskListDescriptors: []); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs index 5e28a880bae..6c2a7936c7d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Options/OptionsStorage.cs @@ -2,9 +2,12 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Settings; @@ -26,6 +29,7 @@ internal class OptionsStorage : IAdvancedSettingsStorage, IDisposable private readonly WritableSettingsStore _writableSettingsStore; private readonly Lazy _telemetryReporter; private readonly JoinableTask _initializeTask; + private ImmutableArray _taskListDescriptors = []; private ISettingsReader? _unifiedSettingsReader; private IDisposable? _unifiedSettingsSubscription; @@ -83,6 +87,11 @@ public bool FormatOnPaste set => SetBool(SettingsNames.FormatOnPaste.LegacyName, value); } + public ImmutableArray TaskListDescriptors + { + get { return _taskListDescriptors; } + } + [ImportingConstructor] public OptionsStorage( SVsServiceProvider synchronousServiceProvider, @@ -101,9 +110,39 @@ public OptionsStorage( var unifiedSettingsManager = await serviceProvider.GetServiceAsync(); _unifiedSettingsReader = unifiedSettingsManager.GetReader(); _unifiedSettingsSubscription = _unifiedSettingsReader.SubscribeToChanges(OnUnifiedSettingsChanged, SettingsNames.AllSettings.Select(s => s.UnifiedName).ToArray()); + + await GetTaskListDescriptorsAsync(joinableTaskContext.Factory, synchronousServiceProvider); }); } + private async Task GetTaskListDescriptorsAsync(JoinableTaskFactory jtf, SVsServiceProvider synchronousServiceProvider) + { + await jtf.SwitchToMainThreadAsync(); + + var taskListService = synchronousServiceProvider.GetService(); + if (taskListService is null) + { + return; + } + + // Not sure why, but the VS Threading analyzer isn't recognizing that we switched to the main thread, above. +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + ErrorHandler.ThrowOnFailure(taskListService.TokenCount(out var count)); + var tokens = new IVsCommentTaskToken[count]; + ErrorHandler.ThrowOnFailure(taskListService.EnumTokens(out var enumerator)); + ErrorHandler.ThrowOnFailure(enumerator.Next((uint)count, tokens, out var numFetched)); + + using var tokensBuilder = new PooledArrayBuilder(); + for (var i = 0; i < numFetched; i++) + { + tokens[i].Text(out var text); + tokensBuilder.Add(text); + } +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread + + _taskListDescriptors = tokensBuilder.ToImmutable(); + } + public async Task OnChangedAsync(Action changed) { await _initializeTask.JoinAsync(); @@ -113,7 +152,16 @@ public async Task OnChangedAsync(Action changed) private EventHandler? _changed; - public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, AutoClosingTags, AutoInsertAttributeQuotes, ColorBackground, CodeBlockBraceOnNextLine, CommitElementsWithSpace, Snippets, LogLevel, FormatOnPaste); + public ClientAdvancedSettings GetAdvancedSettings() => new(FormatOnType, + AutoClosingTags, + AutoInsertAttributeQuotes, + ColorBackground, + CodeBlockBraceOnNextLine, + CommitElementsWithSpace, + Snippets, + LogLevel, + FormatOnPaste, + TaskListDescriptors); public bool GetBool(string name, bool defaultValue) { From ecd935fce77211a6a467dbf11d0d111cabcb78e7 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 14:30:55 +1100 Subject: [PATCH 2/9] Respond to task list item requests --- .../DocumentPullDiagnosticsEndpoint.cs | 43 +++++---- .../Diagnostics/TaskListDiagnosticProvider.cs | 95 +++++++++++++++++++ 2 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs index 1c4e9f50f36..7d060a187b8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs @@ -25,33 +25,27 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; [RazorLanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName)] -internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler?>, ICapabilitiesProvider +internal class DocumentPullDiagnosticsEndpoint( + LanguageServerFeatureOptions languageServerFeatureOptions, + RazorTranslateDiagnosticsService translateDiagnosticsService, + RazorLSPOptionsMonitor razorLSPOptionsMonitor, + IClientConnection clientConnection, + ITelemetryReporter? telemetryReporter) : IRazorRequestHandler?>, ICapabilitiesProvider { - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly IClientConnection _clientConnection; - private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService; - private readonly ITelemetryReporter? _telemetryReporter; + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + private readonly IClientConnection _clientConnection = clientConnection; + private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService = translateDiagnosticsService; + private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor; + private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; private ImmutableDictionary _lastReportedProjectTagHelperCount = ImmutableDictionary.Empty; - public DocumentPullDiagnosticsEndpoint( - LanguageServerFeatureOptions languageServerFeatureOptions, - RazorTranslateDiagnosticsService translateDiagnosticsService, - IClientConnection clientConnection, - ITelemetryReporter? telemetryReporter) - { - _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - _translateDiagnosticsService = translateDiagnosticsService ?? throw new ArgumentNullException(nameof(translateDiagnosticsService)); - _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); - _telemetryReporter = telemetryReporter; - } - public bool MutatesSolutionState => false; public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { serverCapabilities.SupportsDiagnosticRequests = true; serverCapabilities.DiagnosticProvider ??= new(); - serverCapabilities.DiagnosticProvider.DiagnosticKinds = [VSInternalDiagnosticKind.Syntax]; + serverCapabilities.DiagnosticProvider.DiagnosticKinds = [VSInternalDiagnosticKind.Syntax, VSInternalDiagnosticKind.Task]; } public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams request) @@ -71,16 +65,23 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno return default; } - var correlationId = Guid.NewGuid(); - using var __ = _telemetryReporter?.TrackLspRequest(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.DiagnosticsRazorTelemetryThreshold, correlationId); var documentContext = context.DocumentContext; if (documentContext is null) { return null; } - var documentSnapshot = documentContext.Snapshot; + // This endpoint is called for regular diagnostics, and Task List items, and they're handled separately. + if (request.QueryingDiagnosticKind?.Value == VSInternalDiagnosticKind.Task.Value) + { + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + return TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, _razorLSPOptionsMonitor.CurrentValue.TaskListDescriptors); + } + + var correlationId = Guid.NewGuid(); + using var __ = _telemetryReporter?.TrackLspRequest(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.DiagnosticsRazorTelemetryThreshold, correlationId); + var documentSnapshot = documentContext.Snapshot; var razorDiagnostics = await GetRazorDiagnosticsAsync(documentSnapshot, cancellationToken).ConfigureAwait(false); await ReportRZ10012TelemetryAsync(documentContext, razorDiagnostics, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs new file mode 100644 index 00000000000..0ba0104f06a --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; +using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity; +using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range; + +namespace Microsoft.CodeAnalysis.Razor.Diagnostics; + +internal static class TaskListDiagnosticProvider +{ + private static readonly DiagnosticTag[] s_taskItemTags = [VSDiagnosticTags.TaskItem]; + + public static VSInternalDiagnosticReport[] GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray taskListDescriptors) + { + var source = codeDocument.Source.Text; + var tree = codeDocument.GetSyntaxTree(); + + using var _ = ListPool.GetPooledObject(out var diagnostics); + + foreach (var node in tree.Root.DescendantNodes()) + { + if (node is RazorCommentBlockSyntax comment) + { + var i = comment.Comment.SpanStart; + + while (char.IsWhiteSpace(source[i])) + { + i++; + } + + foreach (var token in taskListDescriptors) + { + if (i + token.Length + 2 > comment.EndCommentStar.SpanStart || // Enough room in the comment for the token and some content? + !Matches(source, i, token) || // Does the prefix match? + char.IsLetter(source[i + token.Length + 1])) // Is there something after the prefix, so we don't match "TODOLOL" + { + continue; + } + + AddTaskDiagnostic(diagnostics, source.GetRange(comment.Comment.Span), comment.Comment.Content.Trim()); + break; + } + } + } + + return + [ + new VSInternalDiagnosticReport + { + ResultId = Guid.NewGuid().ToString(), + Diagnostics = [.. diagnostics] + } + ]; + } + + private static bool Matches(SourceText source, int i, string token) + { + for (var j = 0; j < token.Length; j++) + { + if (source.Length < i + j) + { + return false; + } + + if (char.ToLowerInvariant(source[i + j]) != char.ToLowerInvariant(token[j])) + { + return false; + } + } + + return true; + } + + private static void AddTaskDiagnostic(List diagnostics, LspRange range, string message) + { + diagnostics.Add(new LspDiagnostic + { + Code = "TODO", + Message = message, + Source = "Razor", + Severity = LspDiagnosticSeverity.Information, + Range = range, + Tags = s_taskItemTags + }); + } +} From aff7fe5627013ead28d23fd34559a183428acb30 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 14:42:55 +1100 Subject: [PATCH 3/9] Refactor a bit and add cohosting support --- .../DocumentPullDiagnosticsEndpoint.cs | 10 ++++- .../Diagnostics/TaskListDiagnosticProvider.cs | 40 ++++++------------ .../Remote/IRemoteDiagnosticsService.cs | 6 +++ .../Diagnostics/RemoteDiagnosticsService.cs | 21 ++++++++++ .../CohostDocumentPullDiagnosticsEndpoint.cs | 42 ++++++++++++++++++- 5 files changed, 89 insertions(+), 30 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs index 7d060a187b8..578b47e9530 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs @@ -75,7 +75,15 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno if (request.QueryingDiagnosticKind?.Value == VSInternalDiagnosticKind.Task.Value) { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - return TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, _razorLSPOptionsMonitor.CurrentValue.TaskListDescriptors); + var diagnostics = TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, _razorLSPOptionsMonitor.CurrentValue.TaskListDescriptors); + return + [ + new() + { + Diagnostics = [.. diagnostics], + ResultId = Guid.NewGuid().ToString() + } + ]; } var correlationId = Guid.NewGuid(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs index 0ba0104f06a..2b21cc9068c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/TaskListDiagnosticProvider.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; @@ -11,7 +9,6 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity; -using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range; namespace Microsoft.CodeAnalysis.Razor.Diagnostics; @@ -19,12 +16,12 @@ internal static class TaskListDiagnosticProvider { private static readonly DiagnosticTag[] s_taskItemTags = [VSDiagnosticTags.TaskItem]; - public static VSInternalDiagnosticReport[] GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray taskListDescriptors) + public static ImmutableArray GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray taskListDescriptors) { var source = codeDocument.Source.Text; var tree = codeDocument.GetSyntaxTree(); - using var _ = ListPool.GetPooledObject(out var diagnostics); + using var diagnostics = new PooledArrayBuilder(); foreach (var node in tree.Root.DescendantNodes()) { @@ -46,20 +43,22 @@ public static VSInternalDiagnosticReport[] GetTaskListDiagnostics(RazorCodeDocum continue; } - AddTaskDiagnostic(diagnostics, source.GetRange(comment.Comment.Span), comment.Comment.Content.Trim()); + diagnostics.Add(new LspDiagnostic + { + Code = "TODO", + Message = comment.Comment.Content.Trim(), + Source = "Razor", + Severity = LspDiagnosticSeverity.Information, + Range = source.GetRange(comment.Comment.Span), + Tags = s_taskItemTags + }); + break; } } } - return - [ - new VSInternalDiagnosticReport - { - ResultId = Guid.NewGuid().ToString(), - Diagnostics = [.. diagnostics] - } - ]; + return diagnostics.ToImmutable(); } private static bool Matches(SourceText source, int i, string token) @@ -79,17 +78,4 @@ private static bool Matches(SourceText source, int i, string token) return true; } - - private static void AddTaskDiagnostic(List diagnostics, LspRange range, string message) - { - diagnostics.Add(new LspDiagnostic - { - Code = "TODO", - Message = message, - Source = "Razor", - Severity = LspDiagnosticSeverity.Information, - Range = range, - Tags = s_taskItemTags - }); - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs index e8f841af660..3aa6be1954c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs @@ -17,4 +17,10 @@ ValueTask> GetDiagnosticsAsync( RoslynLspDiagnostic[] csharpDiagnostics, RoslynLspDiagnostic[] htmlDiagnostics, CancellationToken cancellationToken); + + ValueTask> GetTaskListDiagnosticsAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + ImmutableArray taskListDescriptors, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs index c017b467ffc..02d9553d947 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs @@ -53,4 +53,25 @@ .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, c .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, htmlDiagnostics, context.Snapshot, cancellationToken).ConfigureAwait(false) ]; } + + public ValueTask> GetTaskListDiagnosticsAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + ImmutableArray taskListDescriptors, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetTaskListDiagnosticsAsync(context, taskListDescriptors, cancellationToken), + cancellationToken); + + private async ValueTask> GetTaskListDiagnosticsAsync( + RemoteDocumentContext context, + ImmutableArray taskListDescriptors, + CancellationToken cancellationToken) + { + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + return TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, taskListDescriptors); + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs index 799c84de1ac..c5a4261d732 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs @@ -19,6 +19,7 @@ using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Settings; using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; using RoslynDiagnostic = Roslyn.LanguageServer.Protocol.Diagnostic; @@ -37,6 +38,7 @@ internal class CohostDocumentPullDiagnosticsEndpoint( IHtmlDocumentSynchronizer htmlDocumentSynchronizer, LSPRequestInvoker requestInvoker, IFilePathService filePathService, + IClientSettingsManager clientSettingsManager, ILoggerFactory loggerFactory) : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider { @@ -44,6 +46,7 @@ internal class CohostDocumentPullDiagnosticsEndpoint( private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; private readonly LSPRequestInvoker _requestInvoker = requestInvoker; private readonly IFilePathService _filePathService = filePathService; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); protected override bool MutatesSolutionState => false; @@ -59,7 +62,7 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie Method = VSInternalMethods.DocumentPullDiagnosticName, RegisterOptions = new VSInternalDiagnosticRegistrationOptions() { - DiagnosticKinds = [VSInternalDiagnosticKind.Syntax] + DiagnosticKinds = [VSInternalDiagnosticKind.Syntax, VSInternalDiagnosticKind.Task] } }]; } @@ -71,7 +74,39 @@ public ImmutableArray GetRegistrations(VSInternalClientCapabilitie => request.TextDocument?.ToRazorTextDocumentIdentifier(); protected override Task HandleRequestAsync(VSInternalDocumentDiagnosticsParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) - => HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken); + { + if (request.QueryingDiagnosticKind?.Value == VSInternalDiagnosticKind.Task.Value) + { + return HandleTaskListItemRequestAsync( + context.TextDocument.AssumeNotNull(), + _clientSettingsManager.GetClientSettings().AdvancedSettings.TaskListDescriptors, + cancellationToken); + } + + return HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken); + } + + private async Task HandleTaskListItemRequestAsync(TextDocument razorDocument, ImmutableArray taskListDescriptors, CancellationToken cancellationToken) + { + var diagnostics = await _remoteServiceInvoker.TryInvokeAsync>( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetTaskListDiagnosticsAsync(solutionInfo, razorDocument.Id, taskListDescriptors, cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (diagnostics.IsDefaultOrEmpty) + { + return null; + } + + return + [ + new() + { + Diagnostics = [.. diagnostics], + ResultId = Guid.NewGuid().ToString() + } + ]; + } private async Task HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken) { @@ -192,6 +227,9 @@ internal readonly struct TestAccessor(CohostDocumentPullDiagnosticsEndpoint inst { public Task HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken) => instance.HandleRequestAsync(razorDocument, cancellationToken); + + public Task HandleTaskListItemRequestAsync(TextDocument razorDocument, ImmutableArray taskListDescriptors, CancellationToken cancellationToken) + => instance.HandleTaskListItemRequestAsync(razorDocument, taskListDescriptors, cancellationToken); } } From 4b9bb187cd45c9bcb1724e599b9565d9f09ad83c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 14:58:56 +1100 Subject: [PATCH 4/9] Add tests --- .../TaskListDiagnosticProviderTest.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Diagnostics/TaskListDiagnosticProviderTest.cs diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Diagnostics/TaskListDiagnosticProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Diagnostics/TaskListDiagnosticProviderTest.cs new file mode 100644 index 00000000000..9c261ac3e72 --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Diagnostics/TaskListDiagnosticProviderTest.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Diagnostics; + +public class TaskListDiagnosticProviderTest(ITestOutputHelper testOutputHelper) : ToolingTestBase(testOutputHelper) +{ + [Fact] + public void TODO() + { + VerifyTODOComments(""" +
+ @*[| TODO: This is it |]*@ +
+ """); + } + + [Fact] + public void TODO_Multiline() + { + VerifyTODOComments(""" +
+ @*[| + TODO: This is it + |]*@ +
+ """); + } + + [Fact] + public void TODOnt() + { + VerifyTODOComments(""" +
+ @* TODONT: This is it *@ +
+ """); + } + + [Fact] + public void DoesntFit() + { + VerifyTODOComments(""" +
+
+ + @* Real *@ + """); + } + + private static void VerifyTODOComments(TestCode input) + { + var codeDocument = TestRazorCodeDocument.Create(input.Text); + codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source)); + var inputText = codeDocument.Source.Text; + + var diagnostics = TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, ["TODO", "ReallyLongPrefix"]); + + var markers = diagnostics.SelectMany(d => + new[] { + (index: inputText.GetTextSpan(d.Range.ToLinePositionSpan()).Start, text: "[|"), + (index: inputText.GetTextSpan(d.Range.ToLinePositionSpan()).End, text:"|]") + }); + + var testOutput = input.Text; + foreach (var (index, text) in markers.OrderByDescending(i => i.index)) + { + testOutput = testOutput.Insert(index, text); + } + + AssertEx.EqualOrDiff(input.OriginalInput, testOutput); + } +} From 45683c5e62e8b204ad2600bdadf71c5098384a21 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 15:58:49 +1100 Subject: [PATCH 5/9] Fix up existing tests --- .../LanguageServer/RazorDiagnosticsBenchmark.cs | 4 +++- .../CodeActions/CodeActionEndToEndTest.NetFx.cs | 3 ++- .../DefaultRazorConfigurationServiceTest.cs | 6 +++--- .../Diagnostics/CSharpDiagnosticsEndToEndTest.cs | 5 +++-- .../Diagnostics/DocumentPullDiagnosticsEndpointTest.cs | 2 ++ .../RazorLSPOptionsMonitorTest.cs | 6 +++--- .../LanguageServer/LanguageServerTestBase.cs | 3 ++- .../Cohost/CohostDocumentPullDiagnosticsTest.cs | 4 +++- .../Settings/ClientSettingsManagerTest.cs | 2 +- 9 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs index 8db0a42fc7c..e1a74b69833 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer; using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; @@ -91,8 +92,9 @@ public void Setup() var languageServer = new ClientNotifierService(Diagnostics!); var documentMappingService = BuildRazorDocumentMappingService(); + var optionsMonitor = Mock.Of(MockBehavior.Strict); var translateDiagnosticsService = new RazorTranslateDiagnosticsService(documentMappingService, loggerFactory); - DocumentPullDiagnosticsEndpoint = new DocumentPullDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, languageServer, telemetryReporter: null); + DocumentPullDiagnosticsEndpoint = new DocumentPullDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, optionsMonitor, languageServer, telemetryReporter: null); } private object BuildDiagnostics() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index b33d3951941..f6bf329286a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -824,7 +824,8 @@ public async Task Handle_GenerateMethod_VaryIndentSize(bool insertSpaces, int ta AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, - CommitElementsWithSpace: true); + CommitElementsWithSpace: true, + TaskListDescriptors: []); var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); await optionsMonitor.UpdateAsync(razorLSPOptions, DisposalToken); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs index 9be85770208..771fc8d9e81 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs @@ -22,7 +22,7 @@ public async Task GetLatestOptionsAsync_ReturnsExpectedOptions() { // Arrange var expectedOptions = new RazorLSPOptions( - FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false, TaskListDescriptors: []); var razorJsonString = """ @@ -94,7 +94,7 @@ public void BuildOptions_VSCodeOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false, TaskListDescriptors: []); var razorJsonString = """ { "format": { @@ -131,7 +131,7 @@ public void BuildOptions_VSOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - FormattingFlags.Enabled, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false); + FormattingFlags.Enabled, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, TaskListDescriptors: []); var razorJsonString = """ { } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs index 6a650134e75..709d7831e1e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs @@ -42,7 +42,7 @@ public void M() [Fact] public async Task Handle_Razor() - { + { var input = """ {|RZ10012:|} @@ -65,7 +65,8 @@ private async Task ValidateDiagnosticsAsync(string input, string? filePath = nul var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); var translateDiagnosticsService = new RazorTranslateDiagnosticsService(DocumentMappingService, LoggerFactory); - var diagnosticsEndPoint = new DocumentPullDiagnosticsEndpoint(LanguageServerFeatureOptions, translateDiagnosticsService, languageServer, telemetryReporter: null); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); + var diagnosticsEndPoint = new DocumentPullDiagnosticsEndpoint(LanguageServerFeatureOptions, translateDiagnosticsService, optionsMonitor, languageServer, telemetryReporter: null); var diagnosticsRequest = new VSInternalDocumentDiagnosticsParams { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs index c930ee00dd7..39278660188 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs @@ -23,10 +23,12 @@ public void ApplyCapabilities_AddsExpectedCapabilities() var razorTranslate = new Mock(MockBehavior.Strict, documentMappingService, LoggerFactory); + var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); var clientConnection = new Mock(MockBehavior.Strict); var endpoint = new DocumentPullDiagnosticsEndpoint( TestLanguageServerFeatureOptions.Instance, razorTranslate.Object, + optionsMonitor, clientConnection.Object, telemetryReporter: null); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs index 973d319eb9e..02695f3f4e1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs @@ -23,7 +23,7 @@ public RazorLSPOptionsMonitorTest(ITestOutputHelper testOutput) public async Task UpdateAsync_Invokes_OnChangeRegistration() { // Arrange - var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, TaskListDescriptors: []); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); @@ -45,7 +45,7 @@ public async Task UpdateAsync_Invokes_OnChangeRegistration() public async Task UpdateAsync_DoesNotInvoke_OnChangeRegistration_AfterDispose() { // Arrange - var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, TaskListDescriptors: []); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); @@ -91,7 +91,7 @@ public async Task UpdateAsync_ConfigReturnsNull_DoesNotInvoke_OnChangeRegistrati public void InitializedOptionsAreCurrent() { // Arrange - var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, TaskListDescriptors: []); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs index 4a343bc1cf1..d08b705eefd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs @@ -153,7 +153,8 @@ private protected static RazorLSPOptionsMonitor GetOptionsMonitor( autoInsertAttributeQuotes, colorBackground, codeBlockBraceOnNextLine, - commitElementsWithSpace); + commitElementsWithSpace, + TaskListDescriptors: []); var optionsMonitor = new RazorLSPOptionsMonitor(configService, options); return optionsMonitor; } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs index 427efba455b..0879cd75025 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Settings; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -144,7 +145,8 @@ private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticRe var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.DocumentPullDiagnosticName, htmlResponse)]); - var endpoint = new CohostDocumentPullDiagnosticsEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, FilePathService, LoggerFactory); + var clientSettingsManager = new ClientSettingsManager([]); + var endpoint = new CohostDocumentPullDiagnosticsEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, FilePathService, clientSettingsManager, LoggerFactory); var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs index 0edfde8f751..af68b773a1c 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs @@ -82,7 +82,7 @@ public void Update_TriggersChangedIfAdvancedSettingsAreDifferent() var manager = new ClientSettingsManager(_clientSettingsChangeTriggers); var called = false; manager.ClientSettingsChanged += (caller, args) => called = true; - var settings = new ClientAdvancedSettings(FormatOnType: false, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: true, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, SnippetSetting: default, LogLevel: default, FormatOnPaste: false); + var settings = new ClientAdvancedSettings(FormatOnType: false, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: true, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, SnippetSetting: default, LogLevel: default, FormatOnPaste: false, TaskListDescriptors: []); // Act manager.Update(settings); From 8202cd50614c756fcaa7eb370a2c234f2033fad8 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 16:04:11 +1100 Subject: [PATCH 6/9] Add a test to ensure we get task list items from Roslyn too --- .../CSharpDiagnosticsEndToEndTest.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs index 709d7831e1e..0329c5fb3ab 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs @@ -52,6 +52,26 @@ public async Task Handle_Razor() await ValidateDiagnosticsAsync(input, "File.razor"); } + [Fact] + public async Task TODOComment() + { + var input = """ + +
+ + @functions + { + public void M() + { + // {|TODO:|}TODO: This should be done + } + } + + """; + + await ValidateDiagnosticsAsync(input); + } + private async Task ValidateDiagnosticsAsync(string input, string? filePath = null) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spans); From b16e8a441f4f4794fb3331ce9a1286878b674796 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 20:24:28 +1100 Subject: [PATCH 7/9] Fix options equality --- .../Hosting/RazorLSPOptions.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs index 4fae44d9dd7..c091ac35ad3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis.Razor.Settings; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; @@ -32,6 +34,13 @@ internal record RazorLSPOptions( CommitElementsWithSpace: true, TaskListDescriptors: []); + public ImmutableArray TaskListDescriptors + { + get; + init => field = value.NullToEmpty(); + + } = TaskListDescriptors.NullToEmpty(); + /// /// Initializes the LSP options with the settings from the passed in client settings, and default values for anything /// not defined in client settings. @@ -64,4 +73,37 @@ private static FormattingFlags GetFormattingFlags(ClientSettings settings) return flags; } + + public virtual bool Equals(RazorLSPOptions? other) + { + return other is not null && + Formatting == other.Formatting && + AutoClosingTags == other.AutoClosingTags && + InsertSpaces == other.InsertSpaces && + TabSize == other.TabSize && + AutoShowCompletion == other.AutoShowCompletion && + AutoListParams == other.AutoListParams && + AutoInsertAttributeQuotes == other.AutoInsertAttributeQuotes && + ColorBackground == other.ColorBackground && + CodeBlockBraceOnNextLine == other.CodeBlockBraceOnNextLine && + CommitElementsWithSpace == other.CommitElementsWithSpace && + TaskListDescriptors.SequenceEqual(other.TaskListDescriptors); + } + + public override int GetHashCode() + { + var hash = HashCodeCombiner.Start(); + hash.Add(Formatting); + hash.Add(AutoClosingTags); + hash.Add(InsertSpaces); + hash.Add(TabSize); + hash.Add(AutoShowCompletion); + hash.Add(AutoListParams); + hash.Add(AutoInsertAttributeQuotes); + hash.Add(ColorBackground); + hash.Add(CodeBlockBraceOnNextLine); + hash.Add(CommitElementsWithSpace); + hash.Add(TaskListDescriptors); + return hash; + } } From e2f2c34c8e010366d99b38290b2e511950adf8aa Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 24 Feb 2025 20:24:34 +1100 Subject: [PATCH 8/9] Fix test --- .../Diagnostics/DocumentPullDiagnosticsEndpointTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs index 39278660188..40d01d987a3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs @@ -42,9 +42,10 @@ public void ApplyCapabilities_AddsExpectedCapabilities() Assert.NotNull(serverCapabilities); Assert.NotNull(serverCapabilities.DiagnosticProvider); Assert.NotNull(serverCapabilities.DiagnosticProvider.DiagnosticKinds); - Assert.Single(serverCapabilities.DiagnosticProvider.DiagnosticKinds); + Assert.Equal(2, serverCapabilities.DiagnosticProvider.DiagnosticKinds.Length); // use the expected value directly; if the underlying library changes values, there is likely a downstream impact Assert.Equal("syntax", serverCapabilities.DiagnosticProvider.DiagnosticKinds[0].Value); + Assert.Equal("task", serverCapabilities.DiagnosticProvider.DiagnosticKinds[1].Value); } } From 8b79a4535c0c60c933f63ad85a1f979be5f40d01 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 25 Feb 2025 20:59:20 +1100 Subject: [PATCH 9/9] Add cohost test --- .../CohostDocumentPullDiagnosticsTest.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs index 339a7f51c90..054aa27c510 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs @@ -136,7 +136,22 @@ public void IJustMetYou() """); - private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticReport[]? htmlResponse = null) + [FuseFact] + public Task TODOComments() + => VerifyDiagnosticsAsync(""" + @using System.Threading.Tasks; + +
+ + @*{|TODO: TODO: This does |}*@ + + @* TODONT: This doesn't *@ + +
+ """, + taskListRequest: true); + + private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticReport[]? htmlResponse = null, bool taskListRequest = false) { UpdateClientInitializationOptions(c => c with { ForceRuntimeCodeGeneration = context.ForceRuntimeCodeGeneration }); @@ -148,7 +163,9 @@ private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticRe var clientSettingsManager = new ClientSettingsManager([]); var endpoint = new CohostDocumentPullDiagnosticsEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, clientSettingsManager, LoggerFactory); - var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken); + var result = taskListRequest + ? await endpoint.GetTestAccessor().HandleTaskListItemRequestAsync(document, ["TODO"], DisposalToken) + : await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken); var markers = result!.SelectMany(d => d.Diagnostics.AssumeNotNull()).SelectMany(d => new[] {