Skip to content

Commit a822584

Browse files
authored
Show TODO Razor comments in the Task List in VS (#11540)
Fixes #4446
2 parents e13dfd5 + eabf32d commit a822584

File tree

20 files changed

+571
-58
lines changed

20 files changed

+571
-58
lines changed

src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using BenchmarkDotNet.Attributes;
1212
using Microsoft.AspNetCore.Razor.Language;
13+
using Microsoft.AspNetCore.Razor.LanguageServer;
1314
using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
1415
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
1516
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
@@ -91,8 +92,9 @@ public void Setup()
9192
var languageServer = new ClientNotifierService(Diagnostics!);
9293
var documentMappingService = BuildRazorDocumentMappingService();
9394

95+
var optionsMonitor = Mock.Of<RazorLSPOptionsMonitor>(MockBehavior.Strict);
9496
var translateDiagnosticsService = new RazorTranslateDiagnosticsService(documentMappingService, loggerFactory);
95-
DocumentPullDiagnosticsEndpoint = new DocumentPullDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, languageServer, telemetryReporter: null);
97+
DocumentPullDiagnosticsEndpoint = new DocumentPullDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, optionsMonitor, languageServer, telemetryReporter: null);
9698
}
9799

98100
private object BuildDiagnostics()

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs

+30-21
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,27 @@
2525
namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
2626

2727
[RazorLanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName)]
28-
internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler<VSInternalDocumentDiagnosticsParams, IEnumerable<VSInternalDiagnosticReport>?>, ICapabilitiesProvider
28+
internal class DocumentPullDiagnosticsEndpoint(
29+
LanguageServerFeatureOptions languageServerFeatureOptions,
30+
RazorTranslateDiagnosticsService translateDiagnosticsService,
31+
RazorLSPOptionsMonitor razorLSPOptionsMonitor,
32+
IClientConnection clientConnection,
33+
ITelemetryReporter? telemetryReporter) : IRazorRequestHandler<VSInternalDocumentDiagnosticsParams, IEnumerable<VSInternalDiagnosticReport>?>, ICapabilitiesProvider
2934
{
30-
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions;
31-
private readonly IClientConnection _clientConnection;
32-
private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService;
33-
private readonly ITelemetryReporter? _telemetryReporter;
35+
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
36+
private readonly IClientConnection _clientConnection = clientConnection;
37+
private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService = translateDiagnosticsService;
38+
private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor;
39+
private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter;
3440
private ImmutableDictionary<ProjectKey, int> _lastReportedProjectTagHelperCount = ImmutableDictionary<ProjectKey, int>.Empty;
3541

36-
public DocumentPullDiagnosticsEndpoint(
37-
LanguageServerFeatureOptions languageServerFeatureOptions,
38-
RazorTranslateDiagnosticsService translateDiagnosticsService,
39-
IClientConnection clientConnection,
40-
ITelemetryReporter? telemetryReporter)
41-
{
42-
_languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions));
43-
_translateDiagnosticsService = translateDiagnosticsService ?? throw new ArgumentNullException(nameof(translateDiagnosticsService));
44-
_clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection));
45-
_telemetryReporter = telemetryReporter;
46-
}
47-
4842
public bool MutatesSolutionState => false;
4943

5044
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
5145
{
5246
serverCapabilities.SupportsDiagnosticRequests = true;
5347
serverCapabilities.DiagnosticProvider ??= new();
54-
serverCapabilities.DiagnosticProvider.DiagnosticKinds = [VSInternalDiagnosticKind.Syntax];
48+
serverCapabilities.DiagnosticProvider.DiagnosticKinds = [VSInternalDiagnosticKind.Syntax, VSInternalDiagnosticKind.Task];
5549
}
5650

5751
public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams request)
@@ -71,16 +65,31 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno
7165
return default;
7266
}
7367

74-
var correlationId = Guid.NewGuid();
75-
using var __ = _telemetryReporter?.TrackLspRequest(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.DiagnosticsRazorTelemetryThreshold, correlationId);
7668
var documentContext = context.DocumentContext;
7769
if (documentContext is null)
7870
{
7971
return null;
8072
}
8173

82-
var documentSnapshot = documentContext.Snapshot;
74+
// This endpoint is called for regular diagnostics, and Task List items, and they're handled separately.
75+
if (request.QueryingDiagnosticKind?.Value == VSInternalDiagnosticKind.Task.Value)
76+
{
77+
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
78+
var diagnostics = TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, _razorLSPOptionsMonitor.CurrentValue.TaskListDescriptors);
79+
return
80+
[
81+
new()
82+
{
83+
Diagnostics = [.. diagnostics],
84+
ResultId = Guid.NewGuid().ToString()
85+
}
86+
];
87+
}
8388

89+
var correlationId = Guid.NewGuid();
90+
using var __ = _telemetryReporter?.TrackLspRequest(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.DiagnosticsRazorTelemetryThreshold, correlationId);
91+
92+
var documentSnapshot = documentContext.Snapshot;
8493
var razorDiagnostics = await GetRazorDiagnosticsAsync(documentSnapshot, cancellationToken).ConfigureAwait(false);
8594

8695
await ReportRZ10012TelemetryAsync(documentContext, razorDiagnostics, cancellationToken).ConfigureAwait(false);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs

+50-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Immutable;
6+
using System.Linq;
57
using Microsoft.CodeAnalysis.Razor.Settings;
8+
using Microsoft.Extensions.Internal;
69

710
namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
811

9-
internal record RazorLSPOptions(
12+
internal sealed record RazorLSPOptions(
1013
FormattingFlags Formatting,
1114
bool AutoClosingTags,
1215
bool InsertSpaces,
@@ -16,7 +19,8 @@ internal record RazorLSPOptions(
1619
bool AutoInsertAttributeQuotes,
1720
bool ColorBackground,
1821
bool CodeBlockBraceOnNextLine,
19-
bool CommitElementsWithSpace)
22+
bool CommitElementsWithSpace,
23+
ImmutableArray<string> TaskListDescriptors)
2024
{
2125
public readonly static RazorLSPOptions Default = new(Formatting: FormattingFlags.All,
2226
AutoClosingTags: true,
@@ -27,7 +31,15 @@ internal record RazorLSPOptions(
2731
AutoInsertAttributeQuotes: true,
2832
ColorBackground: false,
2933
CodeBlockBraceOnNextLine: false,
30-
CommitElementsWithSpace: true);
34+
CommitElementsWithSpace: true,
35+
TaskListDescriptors: []);
36+
37+
public ImmutableArray<string> TaskListDescriptors
38+
{
39+
get;
40+
init => field = value.NullToEmpty();
41+
42+
} = TaskListDescriptors.NullToEmpty();
3143

3244
/// <summary>
3345
/// Initializes the LSP options with the settings from the passed in client settings, and default values for anything
@@ -43,7 +55,8 @@ internal static RazorLSPOptions From(ClientSettings settings)
4355
settings.AdvancedSettings.AutoInsertAttributeQuotes,
4456
settings.AdvancedSettings.ColorBackground,
4557
settings.AdvancedSettings.CodeBlockBraceOnNextLine,
46-
settings.AdvancedSettings.CommitElementsWithSpace);
58+
settings.AdvancedSettings.CommitElementsWithSpace,
59+
settings.AdvancedSettings.TaskListDescriptors);
4760

4861
private static FormattingFlags GetFormattingFlags(ClientSettings settings)
4962
{
@@ -60,4 +73,37 @@ private static FormattingFlags GetFormattingFlags(ClientSettings settings)
6073

6174
return flags;
6275
}
76+
77+
public bool Equals(RazorLSPOptions? other)
78+
{
79+
return other is not null &&
80+
Formatting == other.Formatting &&
81+
AutoClosingTags == other.AutoClosingTags &&
82+
InsertSpaces == other.InsertSpaces &&
83+
TabSize == other.TabSize &&
84+
AutoShowCompletion == other.AutoShowCompletion &&
85+
AutoListParams == other.AutoListParams &&
86+
AutoInsertAttributeQuotes == other.AutoInsertAttributeQuotes &&
87+
ColorBackground == other.ColorBackground &&
88+
CodeBlockBraceOnNextLine == other.CodeBlockBraceOnNextLine &&
89+
CommitElementsWithSpace == other.CommitElementsWithSpace &&
90+
TaskListDescriptors.SequenceEqual(other.TaskListDescriptors);
91+
}
92+
93+
public override int GetHashCode()
94+
{
95+
var hash = HashCodeCombiner.Start();
96+
hash.Add(Formatting);
97+
hash.Add(AutoClosingTags);
98+
hash.Add(InsertSpaces);
99+
hash.Add(TabSize);
100+
hash.Add(AutoShowCompletion);
101+
hash.Add(AutoListParams);
102+
hash.Add(AutoInsertAttributeQuotes);
103+
hash.Add(ColorBackground);
104+
hash.Add(CodeBlockBraceOnNextLine);
105+
hash.Add(CommitElementsWithSpace);
106+
hash.Add(TaskListDescriptors);
107+
return hash;
108+
}
63109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Immutable;
5+
using Microsoft.AspNetCore.Razor.Language;
6+
using Microsoft.AspNetCore.Razor.Language.Syntax;
7+
using Microsoft.AspNetCore.Razor.PooledObjects;
8+
using Microsoft.CodeAnalysis.Text;
9+
using Microsoft.VisualStudio.LanguageServer.Protocol;
10+
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
11+
using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity;
12+
13+
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
14+
15+
internal static class TaskListDiagnosticProvider
16+
{
17+
private static readonly DiagnosticTag[] s_taskItemTags = [VSDiagnosticTags.TaskItem];
18+
19+
public static ImmutableArray<LspDiagnostic> GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray<string> taskListDescriptors)
20+
{
21+
var source = codeDocument.Source.Text;
22+
var tree = codeDocument.GetSyntaxTree();
23+
24+
using var diagnostics = new PooledArrayBuilder<LspDiagnostic>();
25+
26+
foreach (var node in tree.Root.DescendantNodes())
27+
{
28+
if (node is RazorCommentBlockSyntax comment)
29+
{
30+
var i = comment.Comment.SpanStart;
31+
32+
while (char.IsWhiteSpace(source[i]))
33+
{
34+
i++;
35+
}
36+
37+
foreach (var token in taskListDescriptors)
38+
{
39+
if (!CommentMatchesToken(source, comment, i, token))
40+
{
41+
continue;
42+
}
43+
44+
diagnostics.Add(new LspDiagnostic
45+
{
46+
Code = "TODO",
47+
Message = comment.Comment.Content.Trim(),
48+
Source = "Razor",
49+
Severity = LspDiagnosticSeverity.Information,
50+
Range = source.GetRange(comment.Comment.Span),
51+
Tags = s_taskItemTags
52+
});
53+
54+
break;
55+
}
56+
}
57+
}
58+
59+
return diagnostics.ToImmutable();
60+
}
61+
62+
private static bool CommentMatchesToken(SourceText source, RazorCommentBlockSyntax comment, int i, string token)
63+
{
64+
if (i + token.Length + 2 > comment.EndCommentStar.SpanStart)
65+
{
66+
// Not enough room in the comment for the token and some content
67+
return false;
68+
}
69+
70+
for (var j = 0; j < token.Length; j++)
71+
{
72+
if (source.Length < i + j)
73+
{
74+
return false;
75+
}
76+
77+
if (char.ToLowerInvariant(source[i + j]) != char.ToLowerInvariant(token[j]))
78+
{
79+
return false;
80+
}
81+
}
82+
83+
if (char.IsLetter(source[i + token.Length + 1]))
84+
{
85+
// The comment starts with the token, but the next character is a letter, which means it is something like "TODONT"
86+
return false;
87+
}
88+
89+
return true;
90+
}
91+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs

+6
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,10 @@ ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetDiagnosticsAsync(
1717
RoslynLspDiagnostic[] csharpDiagnostics,
1818
RoslynLspDiagnostic[] htmlDiagnostics,
1919
CancellationToken cancellationToken);
20+
21+
ValueTask<ImmutableArray<RoslynLspDiagnostic>> GetTaskListDiagnosticsAsync(
22+
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
23+
JsonSerializableDocumentId documentId,
24+
ImmutableArray<string> taskListDescriptors,
25+
CancellationToken cancellationToken);
2026
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs

+21-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Immutable;
56
using Microsoft.CodeAnalysis.Razor.Logging;
67

78
namespace Microsoft.CodeAnalysis.Razor.Settings;
@@ -32,7 +33,25 @@ internal sealed record ClientSpaceSettings(bool IndentWithTabs, int IndentSize)
3233
public int IndentSize { get; } = IndentSize >= 0 ? IndentSize : throw new ArgumentOutOfRangeException(nameof(IndentSize));
3334
}
3435

35-
internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel, bool FormatOnPaste)
36+
internal sealed record ClientAdvancedSettings(bool FormatOnType,
37+
bool AutoClosingTags,
38+
bool AutoInsertAttributeQuotes,
39+
bool ColorBackground,
40+
bool CodeBlockBraceOnNextLine,
41+
bool CommitElementsWithSpace,
42+
SnippetSetting SnippetSetting,
43+
LogLevel LogLevel,
44+
bool FormatOnPaste,
45+
ImmutableArray<string> TaskListDescriptors)
3646
{
37-
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning, FormatOnPaste: true);
47+
public static readonly ClientAdvancedSettings Default = new(FormatOnType: true,
48+
AutoClosingTags: true,
49+
AutoInsertAttributeQuotes: true,
50+
ColorBackground: false,
51+
CodeBlockBraceOnNextLine: false,
52+
CommitElementsWithSpace: true,
53+
SnippetSetting.All,
54+
LogLevel.Warning,
55+
FormatOnPaste: true,
56+
TaskListDescriptors: []);
3857
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs

+21
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,25 @@ .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, c
5353
.. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, htmlDiagnostics, context.Snapshot, cancellationToken).ConfigureAwait(false)
5454
];
5555
}
56+
57+
public ValueTask<ImmutableArray<LspDiagnostic>> GetTaskListDiagnosticsAsync(
58+
JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo,
59+
JsonSerializableDocumentId documentId,
60+
ImmutableArray<string> taskListDescriptors,
61+
CancellationToken cancellationToken)
62+
=> RunServiceAsync(
63+
solutionInfo,
64+
documentId,
65+
context => GetTaskListDiagnosticsAsync(context, taskListDescriptors, cancellationToken),
66+
cancellationToken);
67+
68+
private static async ValueTask<ImmutableArray<LspDiagnostic>> GetTaskListDiagnosticsAsync(
69+
RemoteDocumentContext context,
70+
ImmutableArray<string> taskListDescriptors,
71+
CancellationToken cancellationToken)
72+
{
73+
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
74+
75+
return TaskListDiagnosticProvider.GetTaskListDiagnostics(codeDocument, taskListDescriptors);
76+
}
5677
}

0 commit comments

Comments
 (0)