File: LanguageServer\CSharpTestLspServerHelpers.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.AspNetCore.Razor.Test.Common.Tooling\Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj (Microsoft.AspNetCore.Razor.Test.Common.Tooling)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common.Mef;
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
 
namespace Microsoft.AspNetCore.Razor.Test.Common.LanguageServer;
 
internal static class CSharpTestLspServerHelpers
{
    private const string EditRangeSetting = "editRange";
 
    public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
        SourceText csharpSourceText,
        Uri csharpDocumentUri,
        VSInternalServerCapabilities serverCapabilities,
        CancellationToken cancellationToken) =>
        CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater: null, cancellationToken);
 
    public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
        SourceText csharpSourceText,
        Uri csharpDocumentUri,
        VSInternalServerCapabilities serverCapabilities,
        Action<VSInternalClientCapabilities> capabilitiesUpdater,
        CancellationToken cancellationToken) =>
        CreateCSharpLspServerAsync(csharpSourceText, csharpDocumentUri, serverCapabilities, new EmptyMappingService(), capabilitiesUpdater, cancellationToken);
 
    public static Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
        SourceText csharpSourceText,
        Uri csharpDocumentUri,
        VSInternalServerCapabilities serverCapabilities,
        IRazorMappingService razorMappingService,
        Action<VSInternalClientCapabilities> capabilitiesUpdater,
        CancellationToken cancellationToken)
    {
        var files = new[]
        {
            (csharpDocumentUri, csharpSourceText)
        };
 
        return CreateCSharpLspServerAsync(files, serverCapabilities, razorMappingService, multiTargetProject: true, capabilitiesUpdater, cancellationToken);
    }
 
    public static async Task<CSharpTestLspServer> CreateCSharpLspServerAsync(
        IEnumerable<(Uri Uri, SourceText SourceText)> files,
        VSInternalServerCapabilities serverCapabilities,
        IRazorMappingService razorMappingService,
        bool multiTargetProject,
        Action<VSInternalClientCapabilities> capabilitiesUpdater,
        CancellationToken cancellationToken)
    {
        var csharpFiles = files.Select(f => new CSharpFile(f.Uri, f.SourceText));
 
        var exportProvider = TestComposition.RoslynFeatures
            .AddParts(typeof(RazorTestLanguageServerFactory))
            .ExportProviderFactory.CreateExportProvider();
 
        var metadataReferences = await ReferenceAssemblies.Default.ResolveAsync(language: LanguageNames.CSharp, cancellationToken);
        metadataReferences = metadataReferences.Add(ReferenceUtil.AspNetLatestComponents);
 
        var workspace = CreateCSharpTestWorkspace(csharpFiles, exportProvider, metadataReferences, razorMappingService, multiTargetProject);
 
        var clientCapabilities = new VSInternalClientCapabilities
        {
            SupportsVisualStudioExtensions = true,
            TextDocument = new TextDocumentClientCapabilities
            {
                Completion = new VSInternalCompletionSetting
                {
                    CompletionListSetting = new()
                    {
                        ItemDefaults = [EditRangeSetting]
                    },
                    CompletionItem = new()
                    {
                        SnippetSupport = true
                    }
                },
                InlayHint = new()
                {
                    ResolveSupport = new InlayHintResolveSupportSetting { Properties = ["tooltip"] }
                }
            },
            SupportsDiagnosticRequests = true,
            Workspace = new()
            {
                Configuration = true
            }
        };
 
        capabilitiesUpdater?.Invoke(clientCapabilities);
 
        return await CSharpTestLspServer.CreateAsync(
            workspace, exportProvider, clientCapabilities, serverCapabilities, cancellationToken);
    }
 
    private static AdhocWorkspace CreateCSharpTestWorkspace(
        IEnumerable<CSharpFile> files,
        ExportProvider exportProvider,
        ImmutableArray<MetadataReference> metadataReferences,
        IRazorMappingService razorMappingService,
        bool multiTargetProject)
    {
        var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(exportProvider);
 
        // Add project and solution to workspace
        var projectInfoNet60 = ProjectInfo.Create(
            id: ProjectId.CreateNewId("TestProject (net6.0)"),
            version: VersionStamp.Default,
            name: "TestProject (net6.0)",
            assemblyName: "TestProject.dll",
            language: LanguageNames.CSharp,
            filePath: @"C:\TestSolution\TestProject.csproj",
            metadataReferences: metadataReferences).WithCompilationOutputInfo(new CompilationOutputInfo().WithAssemblyPath(@"C:\TestSolution\obj\TestProject.dll"));
 
        var projectInfoNet100 = ProjectInfo.Create(
            id: ProjectId.CreateNewId("TestProject (net10.0)"),
            version: VersionStamp.Default,
            name: "TestProject (net10.0)",
            assemblyName: "TestProject.dll",
            language: LanguageNames.CSharp,
            filePath: @"C:\TestSolution\TestProject.csproj",
            metadataReferences: metadataReferences);
 
        ProjectInfo[] projectInfos = multiTargetProject
            ? [projectInfoNet60, projectInfoNet100]
            : [projectInfoNet100];
 
        foreach (var projectInfo in projectInfos)
        {
            workspace.AddProject(projectInfo);
        }
 
        // Add document to workspace. We use an IVT method to create the DocumentInfo variable because there's
        // a special constructor in Roslyn that will help identify the document as belonging to Razor.
        var languageServerFactory = exportProvider.GetExportedValue<AbstractRazorLanguageServerFactoryWrapper>();
 
        var documentCount = 0;
        foreach (var (documentUri, csharpSourceText) in files)
        {
            var documentFilePath = documentUri.GetDocumentFilePath();
            var textAndVersion = TextAndVersion.Create(csharpSourceText, VersionStamp.Default, documentFilePath);
 
            foreach (var projectInfo in projectInfos)
            {
                var documentInfo = languageServerFactory.CreateDocumentInfo(
                    id: DocumentId.CreateNewId(projectInfo.Id),
                    name: "TestDocument" + documentCount,
                    filePath: documentFilePath,
                    loader: TextLoader.From(textAndVersion),
                    razorDocumentServiceProvider: new TestRazorDocumentServiceProvider(razorMappingService));
 
                workspace.AddDocument(documentInfo);
            }
 
            documentCount++;
        }
 
        return workspace;
    }
 
    private record CSharpFile(Uri DocumentUri, SourceText CSharpSourceText);
 
    private class EmptyMappingService : IRazorMappingService
    {
        public Task<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Document document, IEnumerable<TextSpan> spans, CancellationToken cancellationToken)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorMappedSpanResult>();
        }
 
        public Task<ImmutableArray<RazorMappedEditResult>> MapTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorMappedEditResult>();
        }
    }
}