Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

[Shared]
[ExportCSharpVisualBasicStatelessLspService(typeof(RazorDynamicFileChangedHandler))]
[Method("razor/dynamicFileInfoChanged")]
internal class RazorDynamicFileChangedHandler : ILspServiceNotificationHandler<RazorDynamicFileChangedParams>
{
private readonly RazorDynamicFileInfoProvider _razorDynamicFileInfoProvider;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RazorDynamicFileChangedHandler(RazorDynamicFileInfoProvider razorDynamicFileInfoProvider)
{
_razorDynamicFileInfoProvider = razorDynamicFileInfoProvider;
}

public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => false;

public Task HandleNotificationAsync(RazorDynamicFileChangedParams request, RequestContext requestContext, CancellationToken cancellationToken)
{
var filePath = ProtocolConversions.GetDocumentFilePathFromUri(request.RazorDocument.Uri);
_razorDynamicFileInfoProvider.Update(filePath);
return Task.CompletedTask;
}
}
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.Razor;

internal class RazorDynamicFileChangedParams
{
[JsonPropertyName("razorDocument")]
public required TextDocumentIdentifier RazorDocument { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

internal partial class RazorDynamicFileInfoProvider
{
private sealed class TextChangesTextLoader(
TextDocument? document,
IEnumerable<TextChange> changes,
byte[] checksum,
SourceHashAlgorithm checksumAlgorithm,
int? codePage,
Uri razorUri) : TextLoader
{
private readonly Lazy<SourceText> _emptySourceText = new Lazy<SourceText>(() =>
{
var encoding = codePage is null ? null : Encoding.GetEncoding(codePage.Value);
return SourceText.From("", checksumAlgorithm: checksumAlgorithm, encoding: encoding);
});

public override async Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
{
if (document is null)
{
var text = _emptySourceText.Value.WithChanges(changes);
return TextAndVersion.Create(text, VersionStamp.Default.GetNewerVersion());
}

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

// Validate the checksum information so the edits are known to be correct
if (IsSourceTextMatching(sourceText))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@davidwengier @dibarbet this is the logic I was thinking of on how to handle checksums. Telemetry would probably be done on the razor client side unless there's a preference for something else?

Copy link
Member

Choose a reason for hiding this comment

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

We should definitely track it, fine with doing on the Razor side. However if we're doing the checks here (and reporting in Razor) we're going to lose the reason why they were different in telemetry, which might be key to figuring out where things are going wrong.

{
var version = await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
var newText = sourceText.WithChanges(changes);
return TextAndVersion.Create(newText, version.GetNewerVersion());
}

return await GetFullDocumentFromServerAsync(razorUri, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

I would say though we should evaluate this in a month or so - if we never get non-matching texts this should be changed into an assert, and if we do, we should track down where

Copy link
Contributor Author

Choose a reason for hiding this comment

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

💯 should revisit to make sure this number is not high because that indicates perf is suboptimal

}

private bool IsSourceTextMatching(SourceText sourceText)
{
if (sourceText.ChecksumAlgorithm != checksumAlgorithm)
{
return false;
}

if (sourceText.Encoding?.CodePage != codePage)
{
return false;
}

if (!sourceText.GetChecksum().SequenceEqual(checksum))
{
return false;
}

return true;
}

private async Task<TextAndVersion> GetFullDocumentFromServerAsync(Uri razorUri, CancellationToken cancellationToken)
{
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<RazorProvideDynamicFileParams, RazorProvideDynamicFileResponse>(
ProvideRazorDynamicFileInfoMethodName,
new RazorProvideDynamicFileParams
{
RazorDocument = new()
{
Uri = razorUri,
},
FullText = true
},
cancellationToken);

RoslynDebug.AssertNotNull(response.Edits);
RoslynDebug.Assert(response.Edits.IsSingle());

var textChanges = response.Edits.Select(e => new TextChange(e.Span.ToTextSpan(), e.NewText));
var text = _emptySourceText.Value.WithChanges(textChanges);
return TextAndVersion.Create(text, VersionStamp.Default.GetNewerVersion());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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.Composition;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

[Shared]
[Export(typeof(IDynamicFileInfoProvider))]
[Export(typeof(RazorDynamicFileInfoProvider))]
[ExportMetadata("Extensions", new string[] { "cshtml", "razor", })]
internal partial class RazorDynamicFileInfoProvider : IDynamicFileInfoProvider
{
private const string ProvideRazorDynamicFileInfoMethodName = "razor/provideDynamicFileInfo";
private const string RemoveRazorDynamicFileInfoMethodName = "razor/removeDynamicFileInfo";

private readonly Lazy<RazorWorkspaceListenerInitializer> _razorWorkspaceListenerInitializer;
private readonly LanguageServerWorkspaceFactory _workspaceFactory;
private readonly AsyncBatchingWorkQueue<string> _updateWorkQueue;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RazorDynamicFileInfoProvider(
Lazy<RazorWorkspaceListenerInitializer> razorWorkspaceListenerInitializer,
LanguageServerWorkspaceFactory workspaceFactory,
IAsynchronousOperationListenerProvider listenerProvider)
{
_razorWorkspaceListenerInitializer = razorWorkspaceListenerInitializer;
_updateWorkQueue = new AsyncBatchingWorkQueue<string>(
TimeSpan.FromMilliseconds(200),
UpdateAsync,
listenerProvider.GetListener(nameof(RazorDynamicFileInfoProvider)),
CancellationToken.None);
_workspaceFactory = workspaceFactory;
}

public event EventHandler<string>? Updated;

public void Update(string filePath)
{
_updateWorkQueue.AddWork(filePath);
}

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

var razorUri = ProtocolConversions.CreateAbsoluteUri(filePath);
var requestParams = new RazorProvideDynamicFileParams
{
RazorDocument = new()
{
Uri = razorUri
}
};

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<RazorProvideDynamicFileParams, RazorProvideDynamicFileResponse>(
ProvideRazorDynamicFileInfoMethodName, requestParams, cancellationToken);

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

// 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 textDocument = await _workspaceFactory.Workspace.CurrentSolution.GetTextDocumentAsync(response.CSharpDocument, cancellationToken).ConfigureAwait(false);
var textChanges = response.Edits.Select(e => new TextChange(e.Span.ToTextSpan(), e.NewText));
var checksum = Convert.FromBase64String(response.Checksum);
var textLoader = new TextChangesTextLoader(
textDocument,
textChanges,
checksum,
response.ChecksumAlgorithm,
response.SourceEncodingCodePage,
razorUri);

return new DynamicFileInfo(
dynamicFileInfoFilePath,
SourceCodeKind.Regular,
textLoader,
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 RazorRemoveDynamicFileParams
{
CSharpDocument = new()
{
Uri = ProtocolConversions.CreateAbsoluteUri(filePath)
}
};

Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through.");
var clientLanguageServerManager = LanguageServerHost.Instance.GetRequiredLspService<IClientLanguageServerManager>();

return clientLanguageServerManager.SendNotificationAsync(
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();

public override Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
{
return Task.FromResult(TextAndVersion.Create(SourceText.From(""), VersionStamp.Default));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CommonLanguageServerProtocol.Framework;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

[ExportCSharpVisualBasicStatelessLspService(typeof(RazorInitializeHandler)), Shared]
[Method("razor/initialize")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.Razor;

internal class RazorProvideDynamicFileParams
{
[JsonPropertyName("razorDocument")]
public required TextDocumentIdentifier RazorDocument { get; set; }

/// <summary>
/// When true, the full text of the document will be sent over as a single
/// edit instead of diff edits
/// </summary>
[JsonPropertyName("fullText")]
public bool FullText { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

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

[JsonPropertyName("edits")]
public ServerTextChange[]? Edits { get; set; }

[JsonPropertyName("checksum")]
public required string Checksum { get; set; }

[JsonPropertyName("checksumAlgorithm")]
public SourceHashAlgorithm ChecksumAlgorithm { get; set; }

[JsonPropertyName("encodingCodePage")]
public int? SourceEncodingCodePage { 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.Razor;

internal class RazorRemoveDynamicFileParams
{
[JsonPropertyName("csharpDocument")]
public required TextDocumentIdentifier CSharpDocument { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

[Export(typeof(RazorWorkspaceListenerInitializer)), Shared]
internal sealed class RazorWorkspaceListenerInitializer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.Razor;

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

[JsonPropertyName("newText")]
public required string NewText { get; set; }
}
Loading
Loading