Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Update Razor DynamicFile Provider #76050

Merged
merged 15 commits into from
Dec 5, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class RazorDynamicFileChangedParams
{
[JsonPropertyName("razorDocument")]
public required TextDocumentIdentifier RazorDocument { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,61 @@
// See the LICENSE file in the project root for more information.

using System.Composition;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

[Export(typeof(IDynamicFileInfoProvider)), Shared]
[Shared]
[Export(typeof(IDynamicFileInfoProvider))]
[ExportMetadata("Extensions", new string[] { "cshtml", "razor", })]
internal class RazorDynamicFileInfoProvider : IDynamicFileInfoProvider
[ExportCSharpVisualBasicStatelessLspService(typeof(RazorDynamicFileInfoProvider))]
[Method("razor/dynamicFileInfoChanged")]
internal class RazorDynamicFileInfoProvider : IDynamicFileInfoProvider, ILspServiceNotificationHandler<RazorDynamicFileChangedParams>
{
private const string ProvideRazorDynamicFileInfoMethodName = "razor/provideDynamicFileInfo";
private const string RemoveRazorDynamicFileInfoMethodName = "razor/removeDynamicFileInfo";

private class ProvideDynamicFileParams
{
[JsonPropertyName("razorDocument")]
public required TextDocumentIdentifier RazorDocument { get; set; }
}
private readonly Lazy<RazorWorkspaceListenerInitializer> _razorWorkspaceListenerInitializer;
private readonly AsyncBatchingWorkQueue<string> _updateWorkQueue;

private class ProvideDynamicFileResponse
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RazorDynamicFileInfoProvider(
Lazy<RazorWorkspaceListenerInitializer> razorWorkspaceListenerInitializer,
IAsynchronousOperationListenerProvider listenerProvider)
{
[JsonPropertyName("csharpDocument")]
public required TextDocumentIdentifier CSharpDocument { get; set; }
_razorWorkspaceListenerInitializer = razorWorkspaceListenerInitializer;
_updateWorkQueue = new AsyncBatchingWorkQueue<string>(
TimeSpan.FromMilliseconds(200),
UpdateAsync,
listenerProvider.GetListener(nameof(RazorDynamicFileInfoProvider)),
CancellationToken.None);
}

private const string RemoveRazorDynamicFileInfoMethodName = "razor/removeDynamicFileInfo";

private class RemoveDynamicFileParams
{
[JsonPropertyName("csharpDocument")]
public required TextDocumentIdentifier CSharpDocument { get; set; }
}
public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => false;

#pragma warning disable CS0067 // We won't fire the Updated event -- we expect Razor to send us textual changes via didChange instead
public event EventHandler<string>? Updated;
#pragma warning restore CS0067

private readonly Lazy<RazorWorkspaceListenerInitializer> _razorWorkspaceListenerInitializer;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RazorDynamicFileInfoProvider(Lazy<RazorWorkspaceListenerInitializer> razorWorkspaceListenerInitializer)
public Task HandleNotificationAsync(RazorDynamicFileChangedParams request, RequestContext requestContext, CancellationToken cancellationToken)
{
_razorWorkspaceListenerInitializer = razorWorkspaceListenerInitializer;
var path = ProtocolConversions.GetDocumentFilePathFromUri(request.RazorDocument.Uri);
_updateWorkQueue.AddWork(path);
return Task.CompletedTask;
}

public async Task<DynamicFileInfo?> GetDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken)
{
_razorWorkspaceListenerInitializer.Value.NotifyDynamicFile(projectId);

var requestParams = new ProvideDynamicFileParams
var requestParams = new RazorProvideDynamicFileParams
{
RazorDocument = new()
{
Expand All @@ -67,26 +68,44 @@ public RazorDynamicFileInfoProvider(Lazy<RazorWorkspaceListenerInitializer> razo
Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through.");
var clientLanguageServerManager = LanguageServerHost.Instance.GetRequiredLspService<IClientLanguageServerManager>();

var response = await clientLanguageServerManager.SendRequestAsync<ProvideDynamicFileParams, ProvideDynamicFileResponse>(
var response = await clientLanguageServerManager.SendRequestAsync<RazorProvideDynamicFileParams, RazorProvideDynamicFileResponse>(
ProvideRazorDynamicFileInfoMethodName, requestParams, cancellationToken);

// Since we only sent one file over, we should get either zero or one URI back
var responseUri = response.CSharpDocument?.Uri;

if (responseUri == null)
if (response.CSharpDocument is null)
{
return null;
}
else

// Since we only sent one file over, we should get either zero or one URI back
var responseUri = response.CSharpDocument.Uri;
var dynamicFileInfoFilePath = ProtocolConversions.GetDocumentFilePathFromUri(responseUri);

if (response.Edits is not null)
{
var dynamicFileInfoFilePath = ProtocolConversions.GetDocumentFilePathFromUri(responseUri);
return new DynamicFileInfo(dynamicFileInfoFilePath, SourceCodeKind.Regular, EmptyStringTextLoader.Instance, designTimeOnly: true, documentServiceProvider: null);
var workspaceManager = LanguageServerHost.Instance.GetRequiredLspService<LspWorkspaceManager>();
var (_, _1, document) = await workspaceManager.GetLspDocumentInfoAsync(response.CSharpDocument, cancellationToken);

var sourceText = document is null
? SourceText.From("")
: await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

var version = document is null
? VersionStamp.Default
: await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);

var textChanges = response.Edits.Select(e => new TextChange(e.Span.ToTextSpan(), e.NewText));
var newText = sourceText.WithChanges(textChanges);

var textAndVersion = TextAndVersion.Create(newText, version);
return new DynamicFileInfo(dynamicFileInfoFilePath, SourceCodeKind.Regular, TextLoader.From(textAndVersion), designTimeOnly: true, documentServiceProvider: null);
}

return new DynamicFileInfo(dynamicFileInfoFilePath, SourceCodeKind.Regular, EmptyStringTextLoader.Instance, designTimeOnly: true, documentServiceProvider: null);
}

public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken)
{
var notificationParams = new RemoveDynamicFileParams
var notificationParams = new RazorRemoveDynamicFileParams
{
CSharpDocument = new()
{
Expand All @@ -101,6 +120,17 @@ public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string? projectFileP
RemoveRazorDynamicFileInfoMethodName, notificationParams, cancellationToken).AsTask();
}

private ValueTask UpdateAsync(ImmutableSegmentedList<string> paths, CancellationToken token)
{
foreach (var path in paths)
{
token.ThrowIfCancellationRequested();
Updated?.Invoke(this, path);
}

return ValueTask.CompletedTask;
}

private sealed class EmptyStringTextLoader : TextLoader
{
public static readonly TextLoader Instance = new EmptyStringTextLoader();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class RazorProvideDynamicFileParams
{
[JsonPropertyName("razorDocument")]
public required TextDocumentIdentifier RazorDocument { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class RazorProvideDynamicFileResponse
{
[JsonPropertyName("csharpDocument")]
public required TextDocumentIdentifier CSharpDocument { get; set; }

[JsonPropertyName("edits")]
public ServerTextChange[]? Edits { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class RazorRemoveDynamicFileParams
{
[JsonPropertyName("csharpDocument")]
public required TextDocumentIdentifier CSharpDocument { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class ServerTextChange
{
[JsonPropertyName("span")]
public required ServerTextSpan Span { get; set; }

[JsonPropertyName("newText")]
public required string NewText { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal class ServerTextSpan
{
[JsonPropertyName("start")]
public int Start { get; set; }

[JsonPropertyName("length")]
public int Length { get; set; }

public TextSpan ToTextSpan()
=> new(Start, Length);
}
Loading