From 5819e8ce74923c67abb4de1e8bbed8825739588b Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:03:42 +1100 Subject: [PATCH 01/19] Use constructor injection --- .../Semantic/SemanticTokensRangeEndpoint.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index a3cae37499e..b760f43ae30 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -16,12 +16,15 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; [LanguageServerEndpoint(LspEndpointName)] internal sealed class SemanticTokensRangeEndpoint : IRazorRequestHandler, ICapabilitiesProvider { - public const string LspEndpointName = Methods.TextDocumentSemanticTokensRangeName; + private const string LspEndpointName = Methods.TextDocumentSemanticTokensRangeName; + private RazorSemanticTokensLegend? _razorSemanticTokensLegend; + private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService; private readonly ITelemetryReporter? _telemetryReporter; - public SemanticTokensRangeEndpoint(ITelemetryReporter? telemetryReporter) + public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService, ITelemetryReporter? telemetryReporter) { + _semanticTokensInfoService = semanticTokensInfoService; _telemetryReporter = telemetryReporter; } @@ -52,11 +55,10 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam } var documentContext = requestContext.GetRequiredDocumentContext(); - var semanticTokensInfoService = requestContext.GetRequiredService(); var correlationId = Guid.NewGuid(); using var _ = _telemetryReporter?.TrackLspRequest(LspEndpointName, LanguageServerConstants.RazorLanguageServerName, correlationId); - var semanticTokens = await semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, _razorSemanticTokensLegend.AssumeNotNull(), correlationId, cancellationToken).ConfigureAwait(false); + var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, _razorSemanticTokensLegend.AssumeNotNull(), correlationId, cancellationToken).ConfigureAwait(false); var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); requestContext.Logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, request.Range.Start.Line, request.Range.Start.Character, request.Range.End.Line, request.Range.End.Character, request.TextDocument.Uri); From 93f886d173883cabd6d023cf32497f182fb7547e Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:03:51 +1100 Subject: [PATCH 02/19] Use primary constructor --- .../RazorSemanticTokensInfoService.cs | 38 ++++++------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index c6ce6e0d769..332e3e298e0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -24,35 +24,21 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -internal class RazorSemanticTokensInfoService : IRazorSemanticTokensInfoService +internal class RazorSemanticTokensInfoService( + IClientConnection clientConnection, + IRazorDocumentMappingService documentMappingService, + RazorLSPOptionsMonitor razorLSPOptionsMonitor, + LanguageServerFeatureOptions languageServerFeatureOptions, + IRazorLoggerFactory loggerFactory) + : IRazorSemanticTokensInfoService { private const int TokenSize = 5; - private readonly IRazorDocumentMappingService _documentMappingService; - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor; - private readonly IClientConnection _clientConnection; - private readonly ILogger _logger; - - public RazorSemanticTokensInfoService( - IClientConnection clientConnection, - IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, - LanguageServerFeatureOptions languageServerFeatureOptions, - IRazorLoggerFactory loggerFactory) - { - _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); - _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); - _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - - if (loggerFactory is null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - _logger = loggerFactory.CreateLogger(); - } + private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); + private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); + private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + private readonly ILogger _logger = loggerFactory.CreateLogger(); public async Task GetSemanticTokensAsync( TextDocumentIdentifier textDocumentIdentifier, From c44a260662905a895808ac376e317f06713ba974 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:21:27 +1100 Subject: [PATCH 03/19] Move logic from endpoint into service --- .../Semantic/SemanticTokensRangeEndpoint.cs | 40 ++------------ .../IRazorSemanticTokenInfoService.cs | 5 +- .../RazorSemanticTokensInfoService.cs | 53 +++++++++++++++++-- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index b760f43ae30..31c98dccfd3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -1,45 +1,29 @@ // 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.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -[LanguageServerEndpoint(LspEndpointName)] +[LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] internal sealed class SemanticTokensRangeEndpoint : IRazorRequestHandler, ICapabilitiesProvider { - private const string LspEndpointName = Methods.TextDocumentSemanticTokensRangeName; - - private RazorSemanticTokensLegend? _razorSemanticTokensLegend; private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService; - private readonly ITelemetryReporter? _telemetryReporter; - public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService, ITelemetryReporter? telemetryReporter) + public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService) { _semanticTokensInfoService = semanticTokensInfoService; - _telemetryReporter = telemetryReporter; } public bool MutatesSolutionState { get; } = false; public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { - _razorSemanticTokensLegend = new RazorSemanticTokensLegend(clientCapabilities); - - serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions - { - Full = false, - Legend = _razorSemanticTokensLegend.Legend, - Range = true, - }; + _semanticTokensInfoService.ApplyCapabilities(serverCapabilities, clientCapabilities); } public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParams request) @@ -49,25 +33,9 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam public async Task HandleRequestAsync(SemanticTokensRangeParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - var documentContext = requestContext.GetRequiredDocumentContext(); - var correlationId = Guid.NewGuid(); - using var _ = _telemetryReporter?.TrackLspRequest(LspEndpointName, LanguageServerConstants.RazorLanguageServerName, correlationId); - var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, _razorSemanticTokensLegend.AssumeNotNull(), correlationId, cancellationToken).ConfigureAwait(false); - var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); - - requestContext.Logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, request.Range.Start.Line, request.Range.Start.Character, request.Range.End.Line, request.Range.End.Character, request.TextDocument.Uri); - - if (semanticTokens is not null) - { - Debug.Assert(semanticTokens.Data.Length % 5 == 0, $"Number of semantic token-ints should be divisible by 5. Actual number: {semanticTokens.Data.Length}"); - Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); - } + var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, cancellationToken).ConfigureAwait(false); return semanticTokens; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs index a506293458a..d80bd1ca0f1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs @@ -1,14 +1,13 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -internal interface IRazorSemanticTokensInfoService +internal interface IRazorSemanticTokensInfoService : ICapabilitiesProvider { - Task GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, RazorSemanticTokensLegend razorSemanticTokensLegend, Guid correlationId, CancellationToken cancellationToken); + Task GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index 332e3e298e0..039d3aba4d8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Composition; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -24,12 +26,15 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; +[Export(typeof(IRazorSemanticTokensInfoService)), Shared] +[method: ImportingConstructor] internal class RazorSemanticTokensInfoService( IClientConnection clientConnection, IRazorDocumentMappingService documentMappingService, RazorLSPOptionsMonitor razorLSPOptionsMonitor, LanguageServerFeatureOptions languageServerFeatureOptions, - IRazorLoggerFactory loggerFactory) + IRazorLoggerFactory loggerFactory, + ITelemetryReporter? telemetryReporter) : IRazorSemanticTokensInfoService { private const int TokenSize = 5; @@ -39,24 +44,64 @@ internal class RazorSemanticTokensInfoService( private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; + + private RazorSemanticTokensLegend? _razorSemanticTokensLegend; + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + { + _razorSemanticTokensLegend = new RazorSemanticTokensLegend(clientCapabilities); + + serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions + { + Full = false, + Legend = _razorSemanticTokensLegend.Legend, + Range = true, + }; + } public async Task GetSemanticTokensAsync( TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, - RazorSemanticTokensLegend razorSemanticTokensLegend, + CancellationToken cancellationToken) + { + _razorSemanticTokensLegend.AssumeNotNull(); + + var correlationId = Guid.NewGuid(); + using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentSemanticTokensRangeName, LanguageServerConstants.RazorLanguageServerName, correlationId); + + var semanticTokens = await GetSemanticTokensAsync(textDocumentIdentifier, range, documentContext, correlationId, cancellationToken).ConfigureAwait(false); + + var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); + + _logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, range.Start.Line, range.Start.Character, range.End.Line, range.End.Character, textDocumentIdentifier.Uri); + + if (semanticTokens is not null) + { + Debug.Assert(semanticTokens.Data.Length % 5 == 0, $"Number of semantic token-ints should be divisible by 5. Actual number: {semanticTokens.Data.Length}"); + Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); + } + + return semanticTokens; + } + + private async Task GetSemanticTokensAsync( + TextDocumentIdentifier textDocumentIdentifier, + Range range, + VersionedDocumentContext documentContext, Guid correlationId, CancellationToken cancellationToken) { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, razorSemanticTokensLegend, _razorLSPOptionsMonitor.CurrentValue.ColorBackground); + var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend, _razorLSPOptionsMonitor.CurrentValue.ColorBackground); ImmutableArray? csharpSemanticRangesResult = null; try { - csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(codeDocument, textDocumentIdentifier, range, razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); + csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { From de081ab58e8cb1799318406f895638cd9d9d0574 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:33:47 +1100 Subject: [PATCH 04/19] Move IClientConnection to a parameter to prepare for cohosting --- .../Semantic/SemanticTokensRangeEndpoint.cs | 6 +++-- .../IRazorSemanticTokenInfoService.cs | 2 +- .../RazorSemanticTokensInfoService.cs | 26 ++++++++++--------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index 31c98dccfd3..50e3119ad14 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -13,10 +13,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; internal sealed class SemanticTokensRangeEndpoint : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService; + private readonly IClientConnection _clientConnection; - public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService) + public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService, IClientConnection clientConnection) { _semanticTokensInfoService = semanticTokensInfoService; + _clientConnection = clientConnection; } public bool MutatesSolutionState { get; } = false; @@ -35,7 +37,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam { var documentContext = requestContext.GetRequiredDocumentContext(); - var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, cancellationToken).ConfigureAwait(false); + var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(_clientConnection, request.TextDocument, request.Range, documentContext, cancellationToken).ConfigureAwait(false); return semanticTokens; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs index d80bd1ca0f1..371e029f99a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs @@ -9,5 +9,5 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; internal interface IRazorSemanticTokensInfoService : ICapabilitiesProvider { - Task GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, CancellationToken cancellationToken); + Task GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index 039d3aba4d8..ccf926d9df5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -29,7 +29,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; [Export(typeof(IRazorSemanticTokensInfoService)), Shared] [method: ImportingConstructor] internal class RazorSemanticTokensInfoService( - IClientConnection clientConnection, IRazorDocumentMappingService documentMappingService, RazorLSPOptionsMonitor razorLSPOptionsMonitor, LanguageServerFeatureOptions languageServerFeatureOptions, @@ -42,7 +41,6 @@ internal class RazorSemanticTokensInfoService( private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); - private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); private readonly ILogger _logger = loggerFactory.CreateLogger(); private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; @@ -61,6 +59,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } public async Task GetSemanticTokensAsync( + IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, @@ -71,7 +70,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var correlationId = Guid.NewGuid(); using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentSemanticTokensRangeName, LanguageServerConstants.RazorLanguageServerName, correlationId); - var semanticTokens = await GetSemanticTokensAsync(textDocumentIdentifier, range, documentContext, correlationId, cancellationToken).ConfigureAwait(false); + var semanticTokens = await GetSemanticTokensAsync(clientConnection, textDocumentIdentifier, range, documentContext, correlationId, cancellationToken).ConfigureAwait(false); var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); @@ -87,6 +86,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } private async Task GetSemanticTokensAsync( + IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, @@ -96,12 +96,12 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend, _razorLSPOptionsMonitor.CurrentValue.ColorBackground); + var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend.AssumeNotNull(), _razorLSPOptionsMonitor.CurrentValue.ColorBackground); ImmutableArray? csharpSemanticRangesResult = null; try { - csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); + csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(clientConnection, codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -164,6 +164,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra // Internal and virtual for testing only internal virtual async Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, @@ -202,7 +203,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra csharpRanges = new Range[] { csharpRange }; } - var csharpResponse = await GetMatchingCSharpResponseAsync(textDocumentIdentifier, documentVersion, csharpRanges, correlationId, cancellationToken).ConfigureAwait(false); + var csharpResponse = await GetMatchingCSharpResponseAsync(clientConnection, textDocumentIdentifier, documentVersion, csharpRanges, correlationId, cancellationToken).ConfigureAwait(false); // Indicates an issue with retrieving the C# response (e.g. no response or C# is out of sync with us). // Unrecoverable, return default to indicate no change. We've already queued up a refresh request in @@ -338,6 +339,7 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra } private async Task GetMatchingCSharpResponseAsync( + IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, long documentVersion, Range[] csharpRanges, @@ -348,7 +350,7 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra ProvideSemanticTokensResponse? csharpResponse; if (_languageServerFeatureOptions.UsePreciseSemanticTokenRanges) { - csharpResponse = await GetCsharpResponseAsync(parameter, CustomMessageNames.RazorProvidePreciseRangeSemanticTokensEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, parameter, CustomMessageNames.RazorProvidePreciseRangeSemanticTokensEndpoint, cancellationToken).ConfigureAwait(false); // Likely the server doesn't support the new endpoint, fallback to the original one if (csharpResponse?.Tokens is null && csharpRanges.Length > 1) @@ -365,12 +367,12 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra new[] { minimalRange }, parameter.CorrelationId); - csharpResponse = await GetCsharpResponseAsync(newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); } } else { - csharpResponse = await GetCsharpResponseAsync(parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); } if (csharpResponse is null) @@ -436,12 +438,12 @@ internal static bool TryGetSortedCSharpRanges(RazorCodeDocument codeDocument, Ra return true; } - private async Task GetCsharpResponseAsync(ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) + private Task GetCsharpResponseAsync(IClientConnection clientConnection, ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) { - return await _clientConnection.SendRequestAsync( + return clientConnection.SendRequestAsync( lspMethodName, parameter, - cancellationToken).ConfigureAwait(false); + cancellationToken); } private static SemanticRange CSharpDataToSemanticRange( From 075518832317e15753e891dde5ea744b0eeca7ae Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:34:33 +1100 Subject: [PATCH 05/19] Use primary constructor --- .../Semantic/SemanticTokensRangeEndpoint.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index 50e3119ad14..f9f45e8d4f1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -10,16 +10,13 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; [LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] -internal sealed class SemanticTokensRangeEndpoint : IRazorRequestHandler, ICapabilitiesProvider +internal sealed class SemanticTokensRangeEndpoint( + IRazorSemanticTokensInfoService semanticTokensInfoService, + IClientConnection clientConnection) + : IRazorRequestHandler, ICapabilitiesProvider { - private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService; - private readonly IClientConnection _clientConnection; - - public SemanticTokensRangeEndpoint(IRazorSemanticTokensInfoService semanticTokensInfoService, IClientConnection clientConnection) - { - _semanticTokensInfoService = semanticTokensInfoService; - _clientConnection = clientConnection; - } + private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly IClientConnection _clientConnection = clientConnection; public bool MutatesSolutionState { get; } = false; From d216f26870c0a3847b4b031a4b45c9a6f20f8e29 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 23 Dec 2023 22:35:52 +1100 Subject: [PATCH 06/19] Create cohost endpoint --- .../CohostSemanticTokensRangeEndpoint.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs new file mode 100644 index 00000000000..52ae4f5c2b3 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer; +using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; + +[LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] +[ExportRazorStatelessLspService(typeof(CohostSemanticTokensRangeEndpoint))] +[Export(typeof(ICapabilitiesProvider))] +[method: ImportingConstructor] +internal sealed class CohostSemanticTokensRangeEndpoint( + IRazorSemanticTokensInfoService semanticTokensInfoService, + IDocumentContextFactory documentContextFactory, + IRazorLoggerFactory loggerFactory) + : AbstractRazorCohostDocumentRequestHandler, ICapabilitiesProvider +{ + private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + protected override bool MutatesSolutionState => false; + protected override bool RequiresLSPSolution => true; + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(SemanticTokensRangeParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + => _semanticTokensInfoService.ApplyCapabilities(serverCapabilities, clientCapabilities); + + protected override Task HandleRequestAsync(SemanticTokensRangeParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + // TODO: Create document context from request.TextDocument, by looking at request.Solution instead of our project snapshots + var documentContext = _documentContextFactory.TryCreateForOpenDocument(request.TextDocument); + + _logger.LogDebug("[Cohost] Received semantic range request for {requestPath} and got document {documentPath}", request.TextDocument.Uri, documentContext?.FilePath); + + // TODO: We can't MEF import IRazorCohostClientLanguageServerManager in the constructor. We can make this work + // by having it implement a base class, RazorClientConnectionBase or something, that in turn implements + // AbstractRazorLspService (defined in Roslyn) and then move everything from importing IClientConnection + // to importing the new base class, so we can continue to share services. + // + // Until then we have to get the service from the request context. + var clientLanguageServerManager = context.GetRequiredService(); + var clientConnection = new RazorCohostClientConnection(clientLanguageServerManager); + + return _semanticTokensInfoService.GetSemanticTokensAsync(clientConnection, request.TextDocument, request.Range, documentContext.AssumeNotNull(), cancellationToken); + } +} From 0e6bd17330dfdaf859a603607d797a1071887b6d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sun, 24 Dec 2023 09:04:24 +1100 Subject: [PATCH 07/19] Disable semantic tokens in regular server --- .../Extensions/IServiceCollectionExtensions.cs | 13 ++++++++----- .../RazorLanguageServer.cs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index fe893a1f7cc..d6c76cbc454 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -120,16 +120,19 @@ public static void AddHoverServices(this IServiceCollection services) services.AddSingleton(); } - public static void AddSemanticTokensServices(this IServiceCollection services) + public static void AddSemanticTokensServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions) { - services.AddHandlerWithCapabilities(); + if (!featureOptions.UseRazorCohostServer) + { + services.AddHandlerWithCapabilities(); + // Ensure that we don't add the default service if something else has added one. + services.TryAddSingleton(); + } + services.AddHandler(); services.AddSingleton(); services.AddSingleton(); - - // Ensure that we don't add the default service if something else has added one. - services.TryAddSingleton(); } public static void AddCodeActionsServices(this IServiceCollection services) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 1de6064104f..e1aa3be10ad 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -153,7 +153,7 @@ protected override ILspServices ConstructLspServices() services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker); services.AddDiagnosticServices(); - services.AddSemanticTokensServices(); + services.AddSemanticTokensServices(featureOptions); services.AddDocumentManagementServices(featureOptions); services.AddCompletionServices(featureOptions); services.AddFormattingServices(); From 16615cefea408a2c5c90d9f22b30aca105bcdabf Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sun, 24 Dec 2023 09:27:57 +1100 Subject: [PATCH 08/19] Move options retreival code out into the endpoints In some respects this is a temporary thing until we get proper options support in cohosting, but in other respects having the service just take the one option value it actually needs is much nicer --- .../Semantic/SemanticTokensRangeEndpoint.cs | 5 ++++- .../Services/IRazorSemanticTokenInfoService.cs | 2 +- .../Services/RazorSemanticTokensInfoService.cs | 13 +++++++------ .../Cohost/CohostSemanticTokensRangeEndpoint.cs | 9 ++++++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index f9f45e8d4f1..56374e87f89 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -12,10 +12,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; [LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] internal sealed class SemanticTokensRangeEndpoint( IRazorSemanticTokensInfoService semanticTokensInfoService, + RazorLSPOptionsMonitor razorLSPOptionsMonitor, IClientConnection clientConnection) : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor; private readonly IClientConnection _clientConnection = clientConnection; public bool MutatesSolutionState { get; } = false; @@ -33,8 +35,9 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam public async Task HandleRequestAsync(SemanticTokensRangeParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { var documentContext = requestContext.GetRequiredDocumentContext(); + var colorBackground = _razorLSPOptionsMonitor.CurrentValue.ColorBackground; - var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(_clientConnection, request.TextDocument, request.Range, documentContext, cancellationToken).ConfigureAwait(false); + var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(_clientConnection, request.TextDocument, request.Range, documentContext, colorBackground, cancellationToken).ConfigureAwait(false); return semanticTokens; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs index 371e029f99a..2029c027edb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs @@ -9,5 +9,5 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; internal interface IRazorSemanticTokensInfoService : ICapabilitiesProvider { - Task GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, CancellationToken cancellationToken); + Task GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, bool colorBackground, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index ccf926d9df5..82633a2648b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; [method: ImportingConstructor] internal class RazorSemanticTokensInfoService( IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, LanguageServerFeatureOptions languageServerFeatureOptions, IRazorLoggerFactory loggerFactory, ITelemetryReporter? telemetryReporter) @@ -40,7 +39,6 @@ internal class RazorSemanticTokensInfoService( private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); private readonly ILogger _logger = loggerFactory.CreateLogger(); private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; @@ -63,6 +61,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, + bool colorBackground, CancellationToken cancellationToken) { _razorSemanticTokensLegend.AssumeNotNull(); @@ -70,7 +69,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var correlationId = Guid.NewGuid(); using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentSemanticTokensRangeName, LanguageServerConstants.RazorLanguageServerName, correlationId); - var semanticTokens = await GetSemanticTokensAsync(clientConnection, textDocumentIdentifier, range, documentContext, correlationId, cancellationToken).ConfigureAwait(false); + var semanticTokens = await GetSemanticTokensAsync(clientConnection, textDocumentIdentifier, range, documentContext, correlationId, colorBackground, cancellationToken).ConfigureAwait(false); var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); @@ -91,17 +90,19 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V Range range, VersionedDocumentContext documentContext, Guid correlationId, + bool colorBackground, CancellationToken cancellationToken) { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend.AssumeNotNull(), _razorLSPOptionsMonitor.CurrentValue.ColorBackground); + + var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend.AssumeNotNull(), colorBackground); ImmutableArray? csharpSemanticRangesResult = null; try { - csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(clientConnection, codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); + csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(clientConnection, codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, colorBackground, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -169,6 +170,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, @@ -216,7 +218,6 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra using var _ = ArrayBuilderPool.GetPooledObject(out var razorRanges); razorRanges.SetCapacityIfLarger(csharpResponse.Length / TokenSize); - var colorBackground = _razorLSPOptionsMonitor.CurrentValue.ColorBackground; var textClassification = razorSemanticTokensLegend.MarkupTextLiteral; var razorSource = codeDocument.GetSourceText(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs index 52ae4f5c2b3..d47819a04d6 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; @@ -22,11 +23,13 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; [method: ImportingConstructor] internal sealed class CohostSemanticTokensRangeEndpoint( IRazorSemanticTokensInfoService semanticTokensInfoService, + IClientSettingsManager clientSettingsManager, IDocumentContextFactory documentContextFactory, IRazorLoggerFactory loggerFactory) : AbstractRazorCohostDocumentRequestHandler, ICapabilitiesProvider { private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; private readonly ILogger _logger = loggerFactory.CreateLogger(); @@ -55,6 +58,10 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var clientLanguageServerManager = context.GetRequiredService(); var clientConnection = new RazorCohostClientConnection(clientLanguageServerManager); - return _semanticTokensInfoService.GetSemanticTokensAsync(clientConnection, request.TextDocument, request.Range, documentContext.AssumeNotNull(), cancellationToken); + // TODO: This is currently using the "VS" client settings manager, since that's where we are running. In future + // we should create a hook into Roslyn's LSP options infra so we get the option values from the LSP client + var colorBackground = _clientSettingsManager.GetClientSettings().AdvancedSettings.ColorBackground; + + return _semanticTokensInfoService.GetSemanticTokensAsync(clientConnection, request.TextDocument, request.Range, documentContext.AssumeNotNull(), colorBackground, cancellationToken); } } From 0e9d0b3252353dd469b049c8540caa8b4c8711cb Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sun, 24 Dec 2023 10:05:16 +1100 Subject: [PATCH 09/19] Update tests and benchmarks --- .../RazorSemanticTokensBenchmark.cs | 16 +++++++--------- .../RazorSemanticTokensRangeEndpointBenchmark.cs | 11 +++++++---- .../Semantic/SemanticTokensTest.cs | 11 ++++++----- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs index 3dd7dad96c1..f849a43e31a 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs @@ -38,8 +38,6 @@ public class RazorSemanticTokensBenchmark : RazorLanguageServerBenchmarkBase private ProjectSnapshotManagerDispatcher ProjectSnapshotManagerDispatcher { get; set; } - private RazorSemanticTokensLegend SemanticTokensLegend { get; set; } - private string PagesDirectory { get; set; } private string ProjectFilePath { get; set; } @@ -81,8 +79,6 @@ public async Task InitializeRazorSemanticAsync() Character = text.Lines.Last().Span.Length - 1 } }; - - SemanticTokensLegend = new RazorSemanticTokensLegend(new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); } [Benchmark(Description = "Razor Semantic Tokens Range Handling")] @@ -94,11 +90,12 @@ public async Task RazorSemanticTokensRangeAsync() }; var cancellationToken = CancellationToken.None; var documentVersion = 1; - var correlationId = Guid.Empty; await UpdateDocumentAsync(documentVersion, DocumentSnapshot, cancellationToken).ConfigureAwait(false); + + var clientConnection = RazorLanguageServer.GetRequiredService(); await RazorSemanticTokenService.GetSemanticTokensAsync( - textDocumentIdentifier, Range, DocumentContext, SemanticTokensLegend, correlationId, cancellationToken).ConfigureAwait(false); + clientConnection, textDocumentIdentifier, Range, DocumentContext, colorBackground: false, cancellationToken).ConfigureAwait(false); } private async Task UpdateDocumentAsync(int newVersion, IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) @@ -124,6 +121,7 @@ private void EnsureServicesInitialized() { var languageServer = RazorLanguageServer.GetInnerLanguageServerForTesting(); RazorSemanticTokenService = languageServer.GetRequiredService(); + RazorSemanticTokenService.ApplyCapabilities(new(), new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); VersionCache = languageServer.GetRequiredService(); ProjectSnapshotManagerDispatcher = languageServer.GetRequiredService(); } @@ -131,21 +129,21 @@ private void EnsureServicesInitialized() internal class TestRazorSemanticTokensInfoService : RazorSemanticTokensInfoService { public TestRazorSemanticTokensInfoService( - IClientConnection clientConnection, LanguageServerFeatureOptions languageServerFeatureOptions, IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, IRazorLoggerFactory loggerFactory) - : base(clientConnection, documentMappingService, razorLSPOptionsMonitor, languageServerFeatureOptions, loggerFactory) + : base(documentMappingService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) { } // We can't get C# responses without significant amounts of extra work, so let's just shim it for now, any non-Null result is fine. internal override Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs index 7104c3f79af..ca897410a48 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs @@ -74,7 +74,10 @@ public async Task InitializeRazorSemanticAsync() var version = 1; DocumentContext = new VersionedDocumentContext(documentUri, documentSnapshot, projectContext: null, version); Logger = new NoopLogger(); - SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(telemetryReporter: null); + + var razorOptionsMonitor = RazorLanguageServer.GetRequiredService(); + var clientConnection = RazorLanguageServer.GetRequiredService(); + SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(RazorSemanticTokenService, razorOptionsMonitor, clientConnection); SemanticTokensRangeEndpoint.ApplyCapabilities(new(), new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); var text = await DocumentContext.GetSourceTextAsync(CancellationToken.None).ConfigureAwait(false); @@ -151,21 +154,21 @@ private void EnsureServicesInitialized() internal class TestCustomizableRazorSemanticTokensInfoService : RazorSemanticTokensInfoService { public TestCustomizableRazorSemanticTokensInfoService( - IClientConnection clientConnection, LanguageServerFeatureOptions languageServerFeatureOptions, IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, IRazorLoggerFactory loggerFactory) - : base(clientConnection, documentMappingService, razorLSPOptionsMonitor, languageServerFeatureOptions, loggerFactory) + : base(documentMappingService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) { } // We can't get C# responses without significant amounts of extra work, so let's just shim it for now, any non-Null result is fine. internal override Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs index 5cd3f012aaf..88ff982bbc6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs @@ -918,7 +918,7 @@ private async Task AssertSemanticTokensAsync( var service = await CreateServiceAsync(documentContext, csharpTokens, withCSharpBackground, serverSupportsPreciseRanges, precise); var range = GetRange(documentText); - var tokens = await service.GetSemanticTokensAsync(new() { Uri = documentContext.Uri }, range, documentContext, TestRazorSemanticTokensLegend.Instance, Guid.Empty, DisposalToken); + var tokens = await service.GetSemanticTokensAsync(_clientConnection.Object, new() { Uri = documentContext.Uri }, range, documentContext, withCSharpBackground, DisposalToken); var sourceText = await documentContext.GetSourceTextAsync(DisposalToken); AssertSemanticTokensMatchesBaseline(sourceText, tokens?.Data, testName.AssumeNotNull()); @@ -1006,12 +1006,13 @@ private async Task CreateServiceAsync( options.HtmlVirtualDocumentSuffix == "__virtual.html", MockBehavior.Strict); - return new RazorSemanticTokensInfoService( - _clientConnection.Object, + var service = new RazorSemanticTokensInfoService( documentMappingService, - optionsMonitor, featureOptions, - LoggerFactory); + LoggerFactory, + telemetryReporter: null); + service.ApplyCapabilities(new(), new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); + return service; } private async Task GetCSharpSemanticTokensResponseAsync(string documentText, bool precise, bool isRazorFile = false) From bf504f3affe6f8b8ff24383af82bb4ad2cbc82bb Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sun, 24 Dec 2023 22:17:49 +1100 Subject: [PATCH 10/19] Updates after merge --- .../RazorSemanticTokensScrollingBenchmark.cs | 8 +++++--- .../Semantic/SemanticTokensRangeEndpoint.cs | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs index 86dc7be9611..5d52999e867 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs @@ -58,6 +58,7 @@ public async Task InitializeRazorSemanticAsync() DocumentContext = new VersionedDocumentContext(documentUri, documentSnapshot, projectContext: null, version: 1); SemanticTokensLegend = new RazorSemanticTokensLegend(new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); + RazorSemanticTokenService.ApplyCapabilities(new(), new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); var text = await DocumentSnapshot.GetTextAsync().ConfigureAwait(false); Range = new Range @@ -85,13 +86,14 @@ public async Task RazorSemanticTokensRangeScrollingAsync() Uri = DocumentUri }; var cancellationToken = CancellationToken.None; - var correlationId = Guid.Empty; var documentVersion = 1; await UpdateDocumentAsync(documentVersion, DocumentSnapshot).ConfigureAwait(false); var documentLineCount = Range.End.Line; + var clientConnection = RazorLanguageServer.GetRequiredService(); + var lineCount = 0; while (lineCount != documentLineCount) { @@ -102,11 +104,11 @@ public async Task RazorSemanticTokensRangeScrollingAsync() End = new Position(newLineCount, 0) }; await RazorSemanticTokenService!.GetSemanticTokensAsync( + clientConnection, textDocumentIdentifier, range, DocumentContext, - SemanticTokensLegend, - correlationId, + colorBackground: false, cancellationToken); lineCount = newLineCount; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index 5629ba6e298..56374e87f89 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.CommonLanguageServerProtocol.Framework; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; From 18bc37c2806a1999c436b4c312ee216c32bb936e Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 27 Dec 2023 15:00:02 +1100 Subject: [PATCH 11/19] Do code generation as soon as a document opens or changes in cohosting --- .../Cohost/DidChangeHandler.cs | 37 ++++- .../Cohost/DidOpenHandler.cs | 33 +++- .../Cohost/OpenDocumentGenerator.cs | 148 ++++++++++++++++++ 3 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs index d6bf9d299b5..140203fc685 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Text; @@ -14,15 +15,41 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; [Export(typeof(IRazorCohostDidChangeHandler)), Shared] [method: ImportingConstructor] -internal class DidChangeHandler(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService) : IRazorCohostDidChangeHandler +internal class DidChangeHandler( + ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, + RazorProjectService razorProjectService, + ISnapshotResolver snapshotResolver, + OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidChangeHandler { private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; private readonly RazorProjectService _razorProjectService = razorProjectService; + private readonly ISnapshotResolver _snapshotResolver = snapshotResolver; + private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator; - public Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) + public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) { - return _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( - () => _razorProjectService.UpdateDocument(uri.GetAbsoluteOrUNCPath(), sourceText, version), - cancellationToken); + await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(async () => + { + var textDocumentPath = FilePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()); + _razorProjectService.UpdateDocument(textDocumentPath, sourceText, version); + + // We are purposefully trigger code generation here directly, rather than using the project manager events that the above call + // would have triggered, because Cohosting is intended to eventually remove the project manager and its events. We also want + // to eventually remove this code too, and just rely on the source generator, but by keeping the concepts separate we are not + // tying the code to any particular order of feature removal. + if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots)) + { + projectSnapshots = [_snapshotResolver.GetMiscellaneousProject()]; + } + + foreach (var project in projectSnapshots) + { + var document = project.GetDocument(textDocumentPath); + if (document is not null) + { + await _openDocumentGenerator.DocumentOpenedOrChangedAsync(document, version, cancellationToken); + } + } + }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs index f69c4b0bae0..180f9f5da2d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Text; @@ -14,15 +15,37 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; [Export(typeof(IRazorCohostDidOpenHandler)), Shared] [method: ImportingConstructor] -internal class DidOpenHandler(ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService) : IRazorCohostDidOpenHandler +internal class DidOpenHandler( + ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, + RazorProjectService razorProjectService, + ISnapshotResolver snapshotResolver, + OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidOpenHandler { private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; private readonly RazorProjectService _razorProjectService = razorProjectService; + private readonly ISnapshotResolver _snapshotResolver = snapshotResolver; + private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator; - public Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) + public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) { - return _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( - () => _razorProjectService.OpenDocument(uri.GetAbsoluteOrUNCPath(), sourceText, version), - cancellationToken); + await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(async () => + { + var textDocumentPath = FilePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()); + _razorProjectService.OpenDocument(textDocumentPath, sourceText, version); + + if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots)) + { + projectSnapshots = [_snapshotResolver.GetMiscellaneousProject()]; + } + + foreach (var project in projectSnapshots) + { + var document = project.GetDocument(textDocumentPath); + if (document is not null) + { + await _openDocumentGenerator.DocumentOpenedOrChangedAsync(document, version, cancellationToken); + } + } + }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs new file mode 100644 index 00000000000..70ea7b36ef0 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs @@ -0,0 +1,148 @@ +// 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.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; + +[Export(typeof(OpenDocumentGenerator)), Shared] +[method: ImportingConstructor] +internal sealed class OpenDocumentGenerator( + LanguageServerFeatureOptions languageServerFeatureOptions, + LSPDocumentManager documentManager, + CSharpVirtualDocumentAddListener csharpVirtualDocumentAddListener, + JoinableTaskContext joinableTaskContext, + IRazorLoggerFactory loggerFactory) +{ + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); + private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new ArgumentNullException(nameof(documentManager)); + private readonly CSharpVirtualDocumentAddListener _csharpVirtualDocumentAddListener = csharpVirtualDocumentAddListener ?? throw new ArgumentNullException(nameof(csharpVirtualDocumentAddListener)); + private readonly JoinableTaskContext _joinableTaskContext = joinableTaskContext; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + public async Task DocumentOpenedOrChangedAsync(IDocumentSnapshot document, int version, CancellationToken cancellationToken) + { + // These flags exist to workaround things in VS Code, so bringing cohosting to VS Code without also fixing these flags, is very silly. + Debug.Assert(_languageServerFeatureOptions.IncludeProjectKeyInGeneratedFilePath); + Debug.Assert(!_languageServerFeatureOptions.UpdateBuffersForClosedDocuments); + + // Actually do the generation + var generatedOutput = await document.GetGeneratedOutputAsync().ConfigureAwait(false); + + // Now we have to update the LSP buffer etc. + // Fortunate this code will be removed in time + var hostDocumentUri = new Uri(document.FilePath); + + _logger.LogDebug("[Cohost] Updating generated document buffers for {version} of {uri} in {projectKey}", version, hostDocumentUri, document.Project.Key); + + if (_documentManager.TryGetDocument(hostDocumentUri, out var documentSnapshot)) + { + // Html + var htmlVirtualDocumentSnapshot = TryGetHtmlSnapshot(documentSnapshot); + + // CSharp + var csharpVirtualDocumentSnapshot = await TryGetCSharpSnapshotAsync(documentSnapshot, document.Project.Key, version, cancellationToken).ConfigureAwait(false); + + // Buffer work has to be on the UI thread + await _joinableTaskContext.Factory.SwitchToMainThreadAsync(cancellationToken); + + Debug.Assert(htmlVirtualDocumentSnapshot is not null && csharpVirtualDocumentSnapshot is not null || + htmlVirtualDocumentSnapshot is null && csharpVirtualDocumentSnapshot is null, "Found a Html XOR a C# document. Expected both or neither."); + + if (htmlVirtualDocumentSnapshot is not null) + { + _documentManager.UpdateVirtualDocument( + hostDocumentUri, + [new VisualStudioTextChange(0, htmlVirtualDocumentSnapshot.Snapshot.Length, generatedOutput.GetHtmlSourceText().ToString())], + version, + state: null); + } + + if (csharpVirtualDocumentSnapshot is not null) + { + _documentManager.UpdateVirtualDocument( + hostDocumentUri, + csharpVirtualDocumentSnapshot.Uri, + [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, generatedOutput.GetCSharpSourceText().ToString())], + version, + state: null); + return; + } + } + } + + private async Task TryGetCSharpSnapshotAsync(LSPDocumentSnapshot documentSnapshot, ProjectKey projectKey, int version, CancellationToken cancellationToken) + { + if (documentSnapshot.TryGetAllVirtualDocuments(out var virtualDocuments)) + { + if (virtualDocuments is [{ ProjectKey.Id: null }]) + { + // If there is only a single virtual document, and its got a null id, then that means it's in our "misc files" project + // but the server clearly knows about it in a real project. That means its probably new, as Visual Studio opens a buffer + // for a document before we get the notifications about it being added to any projects. Lets try refreshing before + // we worry. + _logger.LogDebug("[Cohost] Refreshing virtual documents, and waiting for them, (for {hostDocumentUri})", documentSnapshot.Uri); + + var task = _csharpVirtualDocumentAddListener.WaitForDocumentAddAsync(cancellationToken); + _documentManager.RefreshVirtualDocuments(); + _ = await task.ConfigureAwait(true); + + // Since we're dealing with snapshots, we have to get the new ones after refreshing + if (!_documentManager.TryGetDocument(documentSnapshot.Uri, out var newDocumentSnapshot) || + !newDocumentSnapshot.TryGetAllVirtualDocuments(out virtualDocuments)) + { + // This should never happen. + // The server clearly wants to tell us about a document in a project, but we don't know which project it's in. + // Sadly there isn't anything we can do here to, we're just in a state where the server and client are out of + // sync with their understanding of the document contents, and since changes come in as a list of changes, + // the user experience is broken. All we can do is hope the user closes and re-opens the document. + Debug.Fail($"Server wants to update {documentSnapshot.Uri} in {projectKey} but we don't know about the document being in any projects"); + _logger.LogError("[Cohost] Server wants to update {hostDocumentUri} in {projectKeyId} by we only know about that document in misc files. Server and client are now out of sync.", documentSnapshot.Uri, projectKey); + return null; + } + } + + foreach (var virtualDocument in virtualDocuments) + { + if (virtualDocument.ProjectKey.Equals(projectKey)) + { + _logger.LogDebug("[Cohost] Found C# virtual doc for {version}: {uri}", version, virtualDocument.Uri); + + return virtualDocument; + } + } + + if (virtualDocuments.Length > 1) + { + // If the particular document supports multiple virtual documents, we don't want to try to update a single one + // TODO: Remove this eventually, as it is a possibly valid state (see comment below) but this assert will help track + // down bugs for now. + Debug.Fail("Multiple virtual documents seem to be supported, but none were updated, which is not impossible, but surprising."); + } + + _logger.LogDebug("[Cohost] Couldn't find any virtual docs for {version} of {uri}", version, documentSnapshot.Uri); + + // Don't know about document, no-op. This can happen if the language server found a project.razor.bin from an old build + // and is sending us updates. + } + + return null; + } + + private static HtmlVirtualDocumentSnapshot? TryGetHtmlSnapshot(LSPDocumentSnapshot documentSnapshot) + { + _ = documentSnapshot.TryGetVirtualDocument(out var htmlVirtualDocumentSnapshot); + return htmlVirtualDocumentSnapshot; + } +} From 4398c6a38bf9ed7340a1d273685e7f79cef8e036 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 27 Dec 2023 15:00:41 +1100 Subject: [PATCH 12/19] Don't publish document changes from the LSP server when cohosting is on This still leaves in place the text sync endpoints, and updates to the LSP servers project system, so that everything else functions as expected --- .../Extensions/IServiceCollectionExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index fe893a1f7cc..2891c5e432c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -228,7 +228,12 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); } - services.AddSingleton(); + // Don't generate documents in the language server if cohost is enabled, let cohost do it. + if (!featureOptions.UseRazorCohostServer) + { + services.AddSingleton(); + } + services.AddSingleton(); services.AddSingleton(); From 34f01cdf17ca8d88d57b1cec12437353654b6933 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 27 Dec 2023 15:04:35 +1100 Subject: [PATCH 13/19] Prevent accidental regressions This also nicely lets us Find All Refs on the Cohost feature flag and see all of the code that can be deleted :) --- .../Endpoints/UpdateCSharpBuffer.cs | 2 ++ .../Endpoints/UpdateHtmlBuffer.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateCSharpBuffer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateCSharpBuffer.cs index e622afd0c35..61234728d3b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateCSharpBuffer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateCSharpBuffer.cs @@ -20,6 +20,8 @@ internal partial class RazorCustomMessageTarget [JsonRpcMethod(CustomMessageNames.RazorUpdateCSharpBufferEndpoint, UseSingleObjectParameterDeserialization = true)] public async Task UpdateCSharpBufferAsync(UpdateBufferRequest request, CancellationToken cancellationToken) { + Debug.Assert(!_languageServerFeatureOptions.UseRazorCohostServer); + if (request is null) { throw new ArgumentNullException(nameof(request)); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateHtmlBuffer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateHtmlBuffer.cs index eb38c1cc21c..a85a5f3748e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateHtmlBuffer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Endpoints/UpdateHtmlBuffer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,8 @@ internal partial class RazorCustomMessageTarget [JsonRpcMethod(CustomMessageNames.RazorUpdateHtmlBufferEndpoint, UseSingleObjectParameterDeserialization = true)] public async Task UpdateHtmlBufferAsync(UpdateBufferRequest request, CancellationToken cancellationToken) { + Debug.Assert(!_languageServerFeatureOptions.UseRazorCohostServer); + if (request is null) { throw new ArgumentNullException(nameof(request)); From 78ad08fe1a58627c03a6559e47e18172caa25d4a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 27 Dec 2023 15:54:19 +1100 Subject: [PATCH 14/19] Regenerate open documents when the project changes --- .../Cohost/OpenDocumentGenerator.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs index 70ea7b36ef0..11efa26e3ea 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -16,14 +17,16 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; -[Export(typeof(OpenDocumentGenerator)), Shared] +[Shared] +[Export(typeof(OpenDocumentGenerator))] +[Export(typeof(IProjectSnapshotChangeTrigger))] [method: ImportingConstructor] internal sealed class OpenDocumentGenerator( LanguageServerFeatureOptions languageServerFeatureOptions, LSPDocumentManager documentManager, CSharpVirtualDocumentAddListener csharpVirtualDocumentAddListener, JoinableTaskContext joinableTaskContext, - IRazorLoggerFactory loggerFactory) + IRazorLoggerFactory loggerFactory) : IProjectSnapshotChangeTrigger { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new ArgumentNullException(nameof(documentManager)); @@ -31,6 +34,8 @@ internal sealed class OpenDocumentGenerator( private readonly JoinableTaskContext _joinableTaskContext = joinableTaskContext; private readonly ILogger _logger = loggerFactory.CreateLogger(); + private ProjectSnapshotManager? _projectManager; + public async Task DocumentOpenedOrChangedAsync(IDocumentSnapshot document, int version, CancellationToken cancellationToken) { // These flags exist to workaround things in VS Code, so bringing cohosting to VS Code without also fixing these flags, is very silly. @@ -145,4 +150,33 @@ [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, ge _ = documentSnapshot.TryGetVirtualDocument(out var htmlVirtualDocumentSnapshot); return htmlVirtualDocumentSnapshot; } + + public void Initialize(ProjectSnapshotManagerBase projectManager) + { + _projectManager = projectManager; + _projectManager.Changed += ProjectManager_Changed; + } + + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) + { + // We only respond to ProjectChanged events, as opens and changes are handled by LSP endpoints, which call into this class too + if (e.Kind == ProjectChangeKind.ProjectChanged) + { + var newProject = e.Newer.AssumeNotNull(); + + foreach (var documentFilePath in newProject.DocumentFilePaths) + { + if (_projectManager!.IsDocumentOpen(documentFilePath) && + newProject.GetDocument(documentFilePath) is { } document && + _documentManager.TryGetDocument(new Uri(document.FilePath), out var documentSnapshot)) + { + // This is not ideal, but we need to re-use the existing snapshot version because our system uses the version + // of the text buffer, but a project change doesn't change the text buffer. + // See https://github.com/dotnet/razor/issues/9197 for more info and some issues this causese + // This should all be moot eventually in Cohosting eventually anyway (ie, this whole file should be deleted) + _ = DocumentOpenedOrChangedAsync(document, documentSnapshot.Version, CancellationToken.None); + } + } + } + } } From 350f67d1a41adb19ef7f606f8b16465cece9ec39 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sun, 24 Dec 2023 21:59:22 +1100 Subject: [PATCH 15/19] Add feature flag to disable the non-cohost server --- .../ConfigurableLanguageServerFeatureOptions.cs | 3 +++ .../DefaultLanguageServerFeatureOptions.cs | 2 ++ .../LanguageServerFeatureOptions.cs | 2 ++ ...VisualStudioWindowsLanguageServerFeatureOptions.cs | 11 +++++++++++ ...icrosoft.VisualStudio.RazorExtension.Custom.pkgdef | 6 ++++++ .../Workspaces/TestLanguageServerFeatureOptions.cs | 4 +++- 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ConfigurableLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ConfigurableLanguageServerFeatureOptions.cs index 37c46402846..fe658810bb3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ConfigurableLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ConfigurableLanguageServerFeatureOptions.cs @@ -24,6 +24,7 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO private readonly bool? _includeProjectKeyInGeneratedFilePath; private readonly bool? _monitorWorkspaceFolderForConfigurationFiles; private readonly bool? _useRazorCohostServer; + private readonly bool? _disableRazorLanguageServer; public override bool SupportsFileManipulation => _supportsFileManipulation ?? _defaults.SupportsFileManipulation; public override string ProjectConfigurationFileName => _projectConfigurationFileName ?? _defaults.ProjectConfigurationFileName; @@ -39,6 +40,7 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO public override bool IncludeProjectKeyInGeneratedFilePath => _includeProjectKeyInGeneratedFilePath ?? _defaults.IncludeProjectKeyInGeneratedFilePath; public override bool MonitorWorkspaceFolderForConfigurationFiles => _monitorWorkspaceFolderForConfigurationFiles ?? _defaults.MonitorWorkspaceFolderForConfigurationFiles; public override bool UseRazorCohostServer => _useRazorCohostServer ?? _defaults.UseRazorCohostServer; + public override bool DisableRazorLanguageServer => _disableRazorLanguageServer ?? _defaults.DisableRazorLanguageServer; public ConfigurableLanguageServerFeatureOptions(string[] args) { @@ -63,6 +65,7 @@ public ConfigurableLanguageServerFeatureOptions(string[] args) TryProcessBoolOption(nameof(IncludeProjectKeyInGeneratedFilePath), ref _includeProjectKeyInGeneratedFilePath, option, args, i); TryProcessBoolOption(nameof(MonitorWorkspaceFolderForConfigurationFiles), ref _monitorWorkspaceFolderForConfigurationFiles, option, args, i); TryProcessBoolOption(nameof(UseRazorCohostServer), ref _useRazorCohostServer, option, args, i); + TryProcessBoolOption(nameof(DisableRazorLanguageServer), ref _disableRazorLanguageServer, option, args, i); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs index 1e7817f5c6e..ef1417bda67 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs @@ -42,4 +42,6 @@ public override bool ReturnCodeActionAndRenamePathsWithPrefixedSlash public override bool MonitorWorkspaceFolderForConfigurationFiles => true; public override bool UseRazorCohostServer => false; + + public override bool DisableRazorLanguageServer => false; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs index c1f8327388a..10c8b85344d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/LanguageServerFeatureOptions.cs @@ -45,4 +45,6 @@ internal abstract class LanguageServerFeatureOptions public abstract bool MonitorWorkspaceFolderForConfigurationFiles { get; } public abstract bool UseRazorCohostServer { get; } + + public abstract bool DisableRazorLanguageServer { get; } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioWindowsLanguageServerFeatureOptions.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioWindowsLanguageServerFeatureOptions.cs index 952c44cfa35..975f5876d07 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioWindowsLanguageServerFeatureOptions.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioWindowsLanguageServerFeatureOptions.cs @@ -16,12 +16,14 @@ internal class VisualStudioWindowsLanguageServerFeatureOptions : LanguageServerF private const string IncludeProjectKeyInGeneratedFilePathFeatureFlag = "Razor.LSP.IncludeProjectKeyInGeneratedFilePath"; private const string UsePreciseSemanticTokenRangesFeatureFlag = "Razor.LSP.UsePreciseSemanticTokenRanges"; private const string UseRazorCohostServerFeatureFlag = "Razor.LSP.UseRazorCohostServer"; + private const string DisableRazorLanguageServerFeatureFlag = "Razor.LSP.DisableRazorLanguageServer"; private readonly LSPEditorFeatureDetector _lspEditorFeatureDetector; private readonly Lazy _showAllCSharpCodeActions; private readonly Lazy _includeProjectKeyInGeneratedFilePath; private readonly Lazy _usePreciseSemanticTokenRanges; private readonly Lazy _useRazorCohostServer; + private readonly Lazy _disableRazorLanguageServer; [ImportingConstructor] public VisualStudioWindowsLanguageServerFeatureOptions(LSPEditorFeatureDetector lspEditorFeatureDetector) @@ -60,6 +62,13 @@ public VisualStudioWindowsLanguageServerFeatureOptions(LSPEditorFeatureDetector var useRazorCohostServer = featureFlags.IsFeatureEnabled(UseRazorCohostServerFeatureFlag, defaultValue: false); return useRazorCohostServer; }); + + _disableRazorLanguageServer = new Lazy(() => + { + var featureFlags = (IVsFeatureFlags)AsyncPackage.GetGlobalService(typeof(SVsFeatureFlags)); + var disableRazorLanguageServer = featureFlags.IsFeatureEnabled(DisableRazorLanguageServerFeatureFlag, defaultValue: false); + return disableRazorLanguageServer; + }); } // We don't currently support file creation operations on VS Codespaces or VS Liveshare @@ -93,4 +102,6 @@ public VisualStudioWindowsLanguageServerFeatureOptions(LSPEditorFeatureDetector public override bool MonitorWorkspaceFolderForConfigurationFiles => false; public override bool UseRazorCohostServer => _useRazorCohostServer.Value; + + public override bool DisableRazorLanguageServer => _disableRazorLanguageServer.Value; } diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef index 46cecf5c420..4649a4f05e2 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef @@ -67,4 +67,10 @@ "Description"="Uses the Razor language server that is cohosted in Roslyn to provide some Razor tooling functionality." "Value"=dword:00000000 "Title"="Use Roslyn Cohost server for Razor (requires restart)" +"PreviewPaneChannels"="IntPreview,int.main" + +[$RootKey$\FeatureFlags\Razor\LSP\DisableRazorLanguageServer] +"Description"="Disables the Razor Language Server so that only the Cohost server is activate. This is probably a bad idea to turn on if you are not on the Razor Tooling team." +"Value"=dword:00000000 +"Title"="Disable Razor Language Server (requires restart)" "PreviewPaneChannels"="IntPreview,int.main" \ No newline at end of file diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs index ac07721dbe4..5075d6ce1c5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestLanguageServerFeatureOptions.cs @@ -46,5 +46,7 @@ public TestLanguageServerFeatureOptions( public override bool MonitorWorkspaceFolderForConfigurationFiles => _monitorWorkspaceFolderForConfigurationFiles; - public override bool UseRazorCohostServer => throw new System.NotImplementedException(); + public override bool UseRazorCohostServer => false; + + public override bool DisableRazorLanguageServer => false; } From fb372760cef742f79e8e05766d0ba3f28f473694 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 27 Dec 2023 16:10:44 +1100 Subject: [PATCH 16/19] Don't start the language server if the feature flag is off --- .../RazorLanguageServerClient.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerClient.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerClient.cs index 2bd7ba62f65..b0ac07d675d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerClient.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerClient.cs @@ -212,6 +212,11 @@ private void ProjectConfigurationFilePathStore_Changed(object sender, ProjectCon private async Task ProjectConfigurationFilePathStore_ChangedAsync(ProjectConfigurationFilePathChangedEventArgs args, CancellationToken cancellationToken) { + if (_languageServerFeatureOptions.DisableRazorLanguageServer) + { + return; + } + try { var parameter = new MonitorProjectConfigurationFilePathParams() @@ -258,6 +263,12 @@ await _requestInvoker.ReinvokeRequestOnServerAsync Date: Thu, 28 Dec 2023 08:06:41 +1100 Subject: [PATCH 17/19] Missed PR feedback --- .../Semantic/Services/RazorSemanticTokensInfoService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index 82633a2648b..a83ee92e054 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -71,13 +71,13 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V var semanticTokens = await GetSemanticTokensAsync(clientConnection, textDocumentIdentifier, range, documentContext, correlationId, colorBackground, cancellationToken).ConfigureAwait(false); - var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); + var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / TokenSize).ToString(Thread.CurrentThread.CurrentCulture); _logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, range.Start.Line, range.Start.Character, range.End.Line, range.End.Character, textDocumentIdentifier.Uri); if (semanticTokens is not null) { - Debug.Assert(semanticTokens.Data.Length % 5 == 0, $"Number of semantic token-ints should be divisible by 5. Actual number: {semanticTokens.Data.Length}"); + Debug.Assert(semanticTokens.Data.Length % TokenSize == 0, $"Number of semantic token-ints should be divisible by {TokenSize}. Actual number: {semanticTokens.Data.Length}"); Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); } From 8f51c5edc282d2b50ccee9b3df351a1e76f91d75 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 28 Dec 2023 15:42:49 +1100 Subject: [PATCH 18/19] PR Feedback --- .../Cohost/DidChangeHandler.cs | 22 +----- .../Cohost/DidOpenHandler.cs | 18 +---- .../Cohost/OpenDocumentGenerator.cs | 78 ++++++++++++------- 3 files changed, 52 insertions(+), 66 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs index 140203fc685..11f5f20fbe9 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidChangeHandler.cs @@ -18,38 +18,20 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; internal class DidChangeHandler( ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService, - ISnapshotResolver snapshotResolver, OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidChangeHandler { private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; private readonly RazorProjectService _razorProjectService = razorProjectService; - private readonly ISnapshotResolver _snapshotResolver = snapshotResolver; private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator; public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) { - await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(async () => + await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { var textDocumentPath = FilePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()); _razorProjectService.UpdateDocument(textDocumentPath, sourceText, version); - // We are purposefully trigger code generation here directly, rather than using the project manager events that the above call - // would have triggered, because Cohosting is intended to eventually remove the project manager and its events. We also want - // to eventually remove this code too, and just rely on the source generator, but by keeping the concepts separate we are not - // tying the code to any particular order of feature removal. - if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots)) - { - projectSnapshots = [_snapshotResolver.GetMiscellaneousProject()]; - } - - foreach (var project in projectSnapshots) - { - var document = project.GetDocument(textDocumentPath); - if (document is not null) - { - await _openDocumentGenerator.DocumentOpenedOrChangedAsync(document, version, cancellationToken); - } - } + return _openDocumentGenerator.DocumentOpenedOrChangedAsync(textDocumentPath, version, cancellationToken); }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs index 180f9f5da2d..6f2c1059688 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/DidOpenHandler.cs @@ -18,34 +18,20 @@ namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; internal class DidOpenHandler( ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, RazorProjectService razorProjectService, - ISnapshotResolver snapshotResolver, OpenDocumentGenerator openDocumentGenerator) : IRazorCohostDidOpenHandler { private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; private readonly RazorProjectService _razorProjectService = razorProjectService; - private readonly ISnapshotResolver _snapshotResolver = snapshotResolver; private readonly OpenDocumentGenerator _openDocumentGenerator = openDocumentGenerator; public async Task HandleAsync(Uri uri, int version, SourceText sourceText, CancellationToken cancellationToken) { - await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(async () => + await await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { var textDocumentPath = FilePathNormalizer.Normalize(uri.GetAbsoluteOrUNCPath()); _razorProjectService.OpenDocument(textDocumentPath, sourceText, version); - if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots)) - { - projectSnapshots = [_snapshotResolver.GetMiscellaneousProject()]; - } - - foreach (var project in projectSnapshots) - { - var document = project.GetDocument(textDocumentPath); - if (document is not null) - { - await _openDocumentGenerator.DocumentOpenedOrChangedAsync(document, version, cancellationToken); - } - } + return _openDocumentGenerator.DocumentOpenedOrChangedAsync(textDocumentPath, version, cancellationToken); }, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs index 11efa26e3ea..f184c3262ac 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/OpenDocumentGenerator.cs @@ -7,12 +7,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; @@ -25,18 +27,47 @@ internal sealed class OpenDocumentGenerator( LanguageServerFeatureOptions languageServerFeatureOptions, LSPDocumentManager documentManager, CSharpVirtualDocumentAddListener csharpVirtualDocumentAddListener, + ISnapshotResolver snapshotResolver, JoinableTaskContext joinableTaskContext, IRazorLoggerFactory loggerFactory) : IProjectSnapshotChangeTrigger { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new ArgumentNullException(nameof(documentManager)); private readonly CSharpVirtualDocumentAddListener _csharpVirtualDocumentAddListener = csharpVirtualDocumentAddListener ?? throw new ArgumentNullException(nameof(csharpVirtualDocumentAddListener)); - private readonly JoinableTaskContext _joinableTaskContext = joinableTaskContext; + private readonly ISnapshotResolver _snapshotResolver = snapshotResolver ?? throw new ArgumentNullException(nameof(snapshotResolver)); + private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskContext.Factory; private readonly ILogger _logger = loggerFactory.CreateLogger(); private ProjectSnapshotManager? _projectManager; - public async Task DocumentOpenedOrChangedAsync(IDocumentSnapshot document, int version, CancellationToken cancellationToken) + public void Initialize(ProjectSnapshotManagerBase projectManager) + { + _projectManager = projectManager; + _projectManager.Changed += ProjectManager_Changed; + } + + public async Task DocumentOpenedOrChangedAsync(string textDocumentPath, int version, CancellationToken cancellationToken) + { + // We are purposefully trigger code generation here directly, rather than using the project manager events that the above call + // would have triggered, because Cohosting is intended to eventually remove the project manager and its events. We also want + // to eventually remove this code too, and just rely on the source generator, but by keeping the concepts separate we are not + // tying the code to any particular order of feature removal. + if (!_snapshotResolver.TryResolveAllProjects(textDocumentPath, out var projectSnapshots)) + { + projectSnapshots = [_snapshotResolver.GetMiscellaneousProject()]; + } + + foreach (var project in projectSnapshots) + { + var document = project.GetDocument(textDocumentPath); + if (document is not null) + { + await UpdateGeneratedDocumentsAsync(document, version, cancellationToken); + } + } + } + + private async Task UpdateGeneratedDocumentsAsync(IDocumentSnapshot document, int version, CancellationToken cancellationToken) { // These flags exist to workaround things in VS Code, so bringing cohosting to VS Code without also fixing these flags, is very silly. Debug.Assert(_languageServerFeatureOptions.IncludeProjectKeyInGeneratedFilePath); @@ -56,17 +87,18 @@ public async Task DocumentOpenedOrChangedAsync(IDocumentSnapshot document, int v // Html var htmlVirtualDocumentSnapshot = TryGetHtmlSnapshot(documentSnapshot); - // CSharp - var csharpVirtualDocumentSnapshot = await TryGetCSharpSnapshotAsync(documentSnapshot, document.Project.Key, version, cancellationToken).ConfigureAwait(false); + // Buffer work has to be on the UI thread, and getting the C# buffers might result in a change to which buffers exist + await _joinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // Buffer work has to be on the UI thread - await _joinableTaskContext.Factory.SwitchToMainThreadAsync(cancellationToken); + // CSharp + var csharpVirtualDocumentSnapshot = await TryGetCSharpSnapshotAsync(documentSnapshot, document.Project.Key, version, cancellationToken).ConfigureAwait(true); Debug.Assert(htmlVirtualDocumentSnapshot is not null && csharpVirtualDocumentSnapshot is not null || htmlVirtualDocumentSnapshot is null && csharpVirtualDocumentSnapshot is null, "Found a Html XOR a C# document. Expected both or neither."); if (htmlVirtualDocumentSnapshot is not null) { + _logger.LogDebug("Updating to version {version}: {virtualDocument}", version, htmlVirtualDocumentSnapshot.Uri); _documentManager.UpdateVirtualDocument( hostDocumentUri, [new VisualStudioTextChange(0, htmlVirtualDocumentSnapshot.Snapshot.Length, generatedOutput.GetHtmlSourceText().ToString())], @@ -76,6 +108,7 @@ [new VisualStudioTextChange(0, htmlVirtualDocumentSnapshot.Snapshot.Length, gene if (csharpVirtualDocumentSnapshot is not null) { + _logger.LogDebug("Updating to version {version}: {virtualDocument}", version, csharpVirtualDocumentSnapshot.Uri); _documentManager.UpdateVirtualDocument( hostDocumentUri, csharpVirtualDocumentSnapshot.Uri, @@ -89,14 +122,15 @@ [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, ge private async Task TryGetCSharpSnapshotAsync(LSPDocumentSnapshot documentSnapshot, ProjectKey projectKey, int version, CancellationToken cancellationToken) { + ThreadHelper.ThrowIfNotOnUIThread(); + if (documentSnapshot.TryGetAllVirtualDocuments(out var virtualDocuments)) { if (virtualDocuments is [{ ProjectKey.Id: null }]) { - // If there is only a single virtual document, and its got a null id, then that means it's in our "misc files" project - // but the server clearly knows about it in a real project. That means its probably new, as Visual Studio opens a buffer - // for a document before we get the notifications about it being added to any projects. Lets try refreshing before - // we worry. + // If there is only a single virtual document, and its got a null id, then that means it's in our "misc files" project. + // That means its probably new, as Visual Studio opens a buffer for a document before we get the notifications about it + // being added to any projects. Lets try refreshing before we worry. _logger.LogDebug("[Cohost] Refreshing virtual documents, and waiting for them, (for {hostDocumentUri})", documentSnapshot.Uri); var task = _csharpVirtualDocumentAddListener.WaitForDocumentAddAsync(cancellationToken); @@ -112,8 +146,8 @@ [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, ge // Sadly there isn't anything we can do here to, we're just in a state where the server and client are out of // sync with their understanding of the document contents, and since changes come in as a list of changes, // the user experience is broken. All we can do is hope the user closes and re-opens the document. + _logger.LogError("[Cohost] Server wants to update {hostDocumentUri} in {projectKeyId} but we only know about that document in misc files. Server and client are now out of sync.", documentSnapshot.Uri, projectKey); Debug.Fail($"Server wants to update {documentSnapshot.Uri} in {projectKey} but we don't know about the document being in any projects"); - _logger.LogError("[Cohost] Server wants to update {hostDocumentUri} in {projectKeyId} by we only know about that document in misc files. Server and client are now out of sync.", documentSnapshot.Uri, projectKey); return null; } } @@ -128,18 +162,8 @@ [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, ge } } - if (virtualDocuments.Length > 1) - { - // If the particular document supports multiple virtual documents, we don't want to try to update a single one - // TODO: Remove this eventually, as it is a possibly valid state (see comment below) but this assert will help track - // down bugs for now. - Debug.Fail("Multiple virtual documents seem to be supported, but none were updated, which is not impossible, but surprising."); - } - - _logger.LogDebug("[Cohost] Couldn't find any virtual docs for {version} of {uri}", version, documentSnapshot.Uri); - - // Don't know about document, no-op. This can happen if the language server found a project.razor.bin from an old build - // and is sending us updates. + _logger.LogError("[Cohost] Couldn't find any virtual docs for {version} of {uri} in {projectKey}", version, documentSnapshot.Uri, projectKey); + Debug.Fail($"Couldn't find any virtual docs for {version} of {documentSnapshot.Uri} in {projectKey}"); } return null; @@ -151,12 +175,6 @@ [new VisualStudioTextChange(0, csharpVirtualDocumentSnapshot.Snapshot.Length, ge return htmlVirtualDocumentSnapshot; } - public void Initialize(ProjectSnapshotManagerBase projectManager) - { - _projectManager = projectManager; - _projectManager.Changed += ProjectManager_Changed; - } - private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { // We only respond to ProjectChanged events, as opens and changes are handled by LSP endpoints, which call into this class too @@ -174,7 +192,7 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) // of the text buffer, but a project change doesn't change the text buffer. // See https://github.com/dotnet/razor/issues/9197 for more info and some issues this causese // This should all be moot eventually in Cohosting eventually anyway (ie, this whole file should be deleted) - _ = DocumentOpenedOrChangedAsync(document, documentSnapshot.Version, CancellationToken.None); + _ = UpdateGeneratedDocumentsAsync(document, documentSnapshot.Version, CancellationToken.None); } } } From dd99886c21667e87c67a7f29828896857776fb1f Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 3 Jan 2024 11:48:35 +1100 Subject: [PATCH 19/19] Typo --- .../Microsoft.VisualStudio.RazorExtension.Custom.pkgdef | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef index 4649a4f05e2..4edcb2ff278 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.Custom.pkgdef @@ -70,7 +70,7 @@ "PreviewPaneChannels"="IntPreview,int.main" [$RootKey$\FeatureFlags\Razor\LSP\DisableRazorLanguageServer] -"Description"="Disables the Razor Language Server so that only the Cohost server is activate. This is probably a bad idea to turn on if you are not on the Razor Tooling team." +"Description"="Disables the Razor Language Server so that only the Cohost server is active. This is probably a bad idea to turn on if you are not on the Razor Tooling team." "Value"=dword:00000000 "Title"="Disable Razor Language Server (requires restart)" "PreviewPaneChannels"="IntPreview,int.main" \ No newline at end of file