File: FileBasedPrograms\CanonicalMiscellaneousFilesProjectProvider.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// 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.Collections.Immutable;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.Extensions.Logging;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
 
internal sealed class CanonicalMiscellaneousFilesProjectProvider : IDisposable
{
    private readonly LanguageServerWorkspaceFactory _workspaceFactory;
    private readonly ILoggerFactory _loggerFactory;
    private readonly AsyncLazy<ImmutableArray<ProjectFileInfo>> _canonicalBuildResult;
    private string? _tempDirectory;
 
    public CanonicalMiscellaneousFilesProjectProvider(LanguageServerWorkspaceFactory workspaceFactory, ILoggerFactory loggerFactory)
    {
        _workspaceFactory = workspaceFactory;
        _loggerFactory = loggerFactory;
        _canonicalBuildResult = AsyncLazy.Create(LoadCanonicalProjectAsync);
    }
 
    public async Task<ImmutableArray<ProjectFileInfo>> GetProjectInfoAsync(string miscDocumentPath, CancellationToken cancellationToken)
    {
        var canonicalInfos = await _canonicalBuildResult.GetValueAsync(cancellationToken);
        var miscDocFileInfo = new DocumentFileInfo(
            filePath: miscDocumentPath,
            logicalPath: Path.GetFileName(miscDocumentPath),
            isLinked: false,
            isGenerated: false,
            folders: []);
 
        var forkedInfos = canonicalInfos.SelectAsArray(info => info with
        {
            FilePath = miscDocumentPath,
            Documents = info.Documents.Add(miscDocFileInfo),
            FileGlobs = [],
        });
 
        return forkedInfos;
    }
 
    private async Task<ImmutableArray<ProjectFileInfo>> LoadCanonicalProjectAsync(CancellationToken cancellationToken)
    {
        // Set the FileBasedProgram feature flag so that '#:' is permitted without errors in rich misc files.
        // This allows us to avoid spurious errors for files which contain '#:' directives yet are not treated
        // as file-based programs (due to not being saved to disk, for example.)
        var virtualProjectXml = $"""
            <Project Sdk="Microsoft.NET.Sdk">
              <PropertyGroup>
                <TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
                <ImplicitUsings>enable</ImplicitUsings>
                <Nullable>enable</Nullable>
                <Features>$(Features);FileBasedProgram</Features>
              </PropertyGroup>
            </Project>
            """;
 
        _tempDirectory = Path.Combine(Path.GetTempPath(), "roslyn-canonical-misc", Guid.NewGuid().ToString());
 
        Directory.CreateDirectory(_tempDirectory);
        var virtualProjectPath = Path.Combine(_tempDirectory, "Canonical.csproj");
 
        await using var buildHostProcessManager = new BuildHostProcessManager(
            _workspaceFactory.HostWorkspace.Services.SolutionServices.GetSupportedLanguages<ICommandLineParserService>(),
            globalMSBuildProperties: [],
            binaryLogPathProvider: null,
            _loggerFactory);
        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, virtualProjectPath, dotnetPath: null, cancellationToken);
        var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken);
        return await loadedFile.GetProjectFileInfosAsync(cancellationToken);
    }
 
    public void Dispose()
    {
        if (_tempDirectory is not null)
        {
            IOUtilities.PerformIO(() => Directory.Delete(_tempDirectory, recursive: true));
        }
    }
}