Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show TODO Razor comments in the Task List in VS #11540

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,8 +92,9 @@ public void Setup()
var languageServer = new ClientNotifierService(Diagnostics!);
var documentMappingService = BuildRazorDocumentMappingService();

var optionsMonitor = Mock.Of<RazorLSPOptionsMonitor>(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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,27 @@
namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;

[RazorLanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName)]
internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternalDocumentDiagnosticsParams, IEnumerable<VSInternalDiagnosticReport>?>, ICapabilitiesProvider
internal class DocumentPullDiagnosticsEndpoint(
LanguageServerFeatureOptions languageServerFeatureOptions,
RazorTranslateDiagnosticsService translateDiagnosticsService,
RazorLSPOptionsMonitor razorLSPOptionsMonitor,
IClientConnection clientConnection,
ITelemetryReporter? telemetryReporter) : IRazorRequestHandler<VSInternalDocumentDiagnosticsParams, IEnumerable<VSInternalDiagnosticReport>?>, 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<ProjectKey, int> _lastReportedProjectTagHelperCount = ImmutableDictionary<ProjectKey, int>.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)
Expand All @@ -71,16 +65,31 @@ 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);
var diagnostics = TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, _razorLSPOptionsMonitor.CurrentValue.TaskListDescriptors);
return
[
new()
{
Diagnostics = [.. diagnostics],
ResultId = Guid.NewGuid().ToString()
}
];
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Razor.Settings;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting;

Expand All @@ -16,7 +19,8 @@ internal record RazorLSPOptions(
bool AutoInsertAttributeQuotes,
bool ColorBackground,
bool CodeBlockBraceOnNextLine,
bool CommitElementsWithSpace)
bool CommitElementsWithSpace,
ImmutableArray<string> TaskListDescriptors)
{
public readonly static RazorLSPOptions Default = new(Formatting: FormattingFlags.All,
AutoClosingTags: true,
Expand All @@ -27,7 +31,15 @@ internal record RazorLSPOptions(
AutoInsertAttributeQuotes: true,
ColorBackground: false,
CodeBlockBraceOnNextLine: false,
CommitElementsWithSpace: true);
CommitElementsWithSpace: true,
TaskListDescriptors: []);

public ImmutableArray<string> TaskListDescriptors
{
get;
init => field = value.NullToEmpty();

} = TaskListDescriptors.NullToEmpty();

/// <summary>
/// Initializes the LSP options with the settings from the passed in client settings, and default values for anything
Expand All @@ -43,7 +55,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)
{
Expand All @@ -60,4 +73,37 @@ private static FormattingFlags GetFormattingFlags(ClientSettings settings)

return flags;
}

public virtual bool Equals(RazorLSPOptions? other)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you intend for new records to be derived from RazorLSPOptions? If not, please mark RazorLSPOptions as sealed. If you do want RazorLSPOptions to be subtyped, add a check here to compare EqualityContract and add it to `GetHashCode() as well. Otherwise, equality between subtypes will be incorrect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this virtual rather than override?

{
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

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;

namespace Microsoft.CodeAnalysis.Razor.Diagnostics;

internal static class TaskListDiagnosticProvider
{
private static readonly DiagnosticTag[] s_taskItemTags = [VSDiagnosticTags.TaskItem];

public static ImmutableArray<LspDiagnostic> GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray<string> taskListDescriptors)
{
var source = codeDocument.Source.Text;
var tree = codeDocument.GetSyntaxTree();

using var diagnostics = new PooledArrayBuilder<LspDiagnostic>();

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"
Comment on lines +39 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like the first and last checks could be pulled into Matches. At the very least, the last check could be moved after the loop in Matches so that all of the character tests are in one place.

{
continue;
}

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 diagnostics.ToImmutable();
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetDiagnosticsAsync(
RoslynLspDiagnostic[] csharpDiagnostics,
RoslynLspDiagnostic[] htmlDiagnostics,
CancellationToken cancellationToken);

ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetTaskListDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
ImmutableArray<string> taskListDescriptors,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string> 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: []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,25 @@ .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, c
.. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, htmlDiagnostics, context.Snapshot, cancellationToken).ConfigureAwait(false)
];
}

public ValueTask<ImmutableArray<LspDiagnostic>> GetTaskListDiagnosticsAsync(
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
JsonSerializableDocumentId documentId,
ImmutableArray<string> taskListDescriptors,
CancellationToken cancellationToken)
=> RunServiceAsync(
solutionInfo,
documentId,
context => GetTaskListDiagnosticsAsync(context, taskListDescriptors, cancellationToken),
cancellationToken);

private async ValueTask<ImmutableArray<LspDiagnostic>> GetTaskListDiagnosticsAsync(
RemoteDocumentContext context,
ImmutableArray<string> taskListDescriptors,
CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);

return TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, taskListDescriptors);
}
}
Loading