File: Internal\Analyzer\CSharp\CSharpCopilotCodeAnalysisService.ReflectionWrapper.cs
Web Access
Project: src\src\EditorFeatures\ExternalAccess\Copilot\Microsoft.CodeAnalysis.ExternalAccess.Copilot.csproj (Microsoft.CodeAnalysis.ExternalAccess.Copilot)
// 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;
using System.Collections.Immutable;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.ServiceBroker;
using IServiceProvider = System.IServiceProvider;
 
namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.Analyzer.CSharp;
 
using AnalyzeDocumentAsyncDelegateType = Func<Document, TextSpan?, string, CancellationToken, Task<ImmutableArray<Diagnostic>>>;
using GetAvailablePromptTitlesAsyncDelegateType = Func<Document, CancellationToken, Task<ImmutableArray<string>>>;
using GetCachedDiagnosticsAsyncDelegateType = Func<Document, string, CancellationToken, Task<ImmutableArray<Diagnostic>>>;
using IsAvailableAsyncDelegateType = Func<CancellationToken, Task<bool>>;
using StartRefinementSessionAsyncDelegateType = Func<Document, Document, Diagnostic?, CancellationToken, Task>;
using GetOnTheFlyDocsAsyncDelegateType = Func<string, ImmutableArray<string>, string, CancellationToken, Task<string>>;
using IsAnyExclusionAsyncDelegateType = Func<CancellationToken, Task<bool>>;
using IsFileExcludedAsyncDelegateType = Func<string, CancellationToken, Task<bool>>;
 
internal sealed partial class CSharpCopilotCodeAnalysisService
{
    // A temporary helper to get access to the implementation of IExternalCSharpCopilotCodeAnalysisService, until it can be MEF exported.
    private sealed class ReflectionWrapper : IExternalCSharpCopilotCodeAnalysisService
    {
        private const string CopilotRoslynDllName = "Microsoft.VisualStudio.Copilot.Roslyn, Version=0.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
        private const string InternalCSharpCopilotAnalyzerTypeFullName = "Microsoft.VisualStudio.Copilot.Roslyn.Analyzer.InternalCSharpCopilotAnalyzer";
 
        private const string IsAvailableAsyncMethodName = "IsAvailableAsync";
        private const string GetAvailablePromptTitlesAsyncMethodName = "GetAvailablePromptTitlesAsync";
        private const string AnalyzeDocumentAsyncMethodName = "AnalyzeDocumentAsync";
        private const string GetCachedDiagnosticsAsyncMethodName = "GetCachedDiagnosticsAsync";
        private const string StartRefinementSessionAsyncMethodName = "StartRefinementSessionAsync";
        private const string GetOnTheFlyDocsAsyncMethodName = "GetOnTheFlyDocsAsync";
        private const string IsFileExcludedAsyncMethodName = "IsFileExcludedAsync";
 
        // Create and cache closed delegate to ensure we use a singleton object and with better performance.
        private readonly Type? _analyzerType;
        private readonly object? _analyzerInstance;
        private readonly Lazy<IsAvailableAsyncDelegateType?> _lazyIsAvailableAsyncDelegate;
        private readonly Lazy<GetAvailablePromptTitlesAsyncDelegateType?> _lazyGetAvailablePromptTitlesAsyncDelegate;
        private readonly Lazy<AnalyzeDocumentAsyncDelegateType?> _lazyAnalyzeDocumentAsyncDelegate;
        private readonly Lazy<GetCachedDiagnosticsAsyncDelegateType?> _lazyGetCachedDiagnosticsAsyncDelegate;
        private readonly Lazy<StartRefinementSessionAsyncDelegateType?> _lazyStartRefinementSessionAsyncDelegate;
        private readonly Lazy<GetOnTheFlyDocsAsyncDelegateType?> _lazyGetOnTheFlyDocsAsyncDelegate;
        private readonly Lazy<IsFileExcludedAsyncDelegateType?> _lazyIsFileExcludedAsyncDelegate;
 
        public ReflectionWrapper(IServiceProvider serviceProvider, IVsService<SVsBrokeredServiceContainer, IBrokeredServiceContainer> brokeredServiceContainer)
        {
            try
            {
                var assembly = Assembly.Load(CopilotRoslynDllName);
                var analyzerType = assembly.GetType(InternalCSharpCopilotAnalyzerTypeFullName);
                if (analyzerType is not null)
                {
                    var analyzerInstance = Activator.CreateInstance(analyzerType, serviceProvider, brokeredServiceContainer);
                    if (analyzerInstance is not null)
                    {
                        _analyzerType = analyzerType;
                        _analyzerInstance = analyzerInstance;
                    }
                }
            }
            catch
            {
                // Catch all here since failure is expected if user has no copilot chat or an older version of it installed.
            }
 
            _lazyIsAvailableAsyncDelegate = new(CreateIsAvailableAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyGetAvailablePromptTitlesAsyncDelegate = new(CreateGetAvailablePromptTitlesAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyAnalyzeDocumentAsyncDelegate = new(CreateAnalyzeDocumentAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyGetCachedDiagnosticsAsyncDelegate = new(CreateGetCachedDiagnosticsAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyStartRefinementSessionAsyncDelegate = new(CreateStartRefinementSessionAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyGetOnTheFlyDocsAsyncDelegate = new(CreateGetOnTheFlyDocsAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
            _lazyIsFileExcludedAsyncDelegate = new(CreateIsFileExcludedAsyncDelegate, LazyThreadSafetyMode.PublicationOnly);
        }
 
        private T? CreateDelegate<T>(string methodName, Type[] types) where T : Delegate
        {
            try
            {
                if (_analyzerInstance is null || _analyzerType is null)
                    return null;
 
                if (_analyzerType.GetMethod(methodName, types) is MethodInfo methodInfo)
                    return (T)Delegate.CreateDelegate(typeof(T), _analyzerInstance, methodInfo);
            }
            catch
            {
                // Catch all here since failure is expected if user has no copilot chat or an older version of it installed
            }
 
            return null;
        }
 
        private IsAvailableAsyncDelegateType? CreateIsAvailableAsyncDelegate()
            => CreateDelegate<IsAvailableAsyncDelegateType>(IsAvailableAsyncMethodName, [typeof(CancellationToken)]);
 
        private GetAvailablePromptTitlesAsyncDelegateType? CreateGetAvailablePromptTitlesAsyncDelegate()
            => CreateDelegate<GetAvailablePromptTitlesAsyncDelegateType>(GetAvailablePromptTitlesAsyncMethodName, [typeof(Document), typeof(CancellationToken)]);
 
        private AnalyzeDocumentAsyncDelegateType? CreateAnalyzeDocumentAsyncDelegate()
            => CreateDelegate<AnalyzeDocumentAsyncDelegateType>(AnalyzeDocumentAsyncMethodName, [typeof(Document), typeof(TextSpan?), typeof(string), typeof(CancellationToken)]);
 
        private GetCachedDiagnosticsAsyncDelegateType? CreateGetCachedDiagnosticsAsyncDelegate()
            => CreateDelegate<GetCachedDiagnosticsAsyncDelegateType>(GetCachedDiagnosticsAsyncMethodName, [typeof(Document), typeof(string), typeof(CancellationToken)]);
 
        private StartRefinementSessionAsyncDelegateType? CreateStartRefinementSessionAsyncDelegate()
            => CreateDelegate<StartRefinementSessionAsyncDelegateType>(StartRefinementSessionAsyncMethodName, [typeof(Document), typeof(Document), typeof(Diagnostic), typeof(CancellationToken)]);
 
        private GetOnTheFlyDocsAsyncDelegateType? CreateGetOnTheFlyDocsAsyncDelegate()
            => CreateDelegate<GetOnTheFlyDocsAsyncDelegateType>(GetOnTheFlyDocsAsyncMethodName, [typeof(string), typeof(ImmutableArray<string>), typeof(string), typeof(CancellationToken)]);
 
        private IsFileExcludedAsyncDelegateType? CreateIsFileExcludedAsyncDelegate()
            => CreateDelegate<IsFileExcludedAsyncDelegateType>(IsFileExcludedAsyncMethodName, [typeof(string), typeof(CancellationToken)]);
 
        public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
        {
            if (_lazyIsAvailableAsyncDelegate.Value is null)
                return false;
 
            return await _lazyIsAvailableAsyncDelegate.Value(cancellationToken).ConfigureAwait(false);
        }
 
        public async Task<ImmutableArray<string>> GetAvailablePromptTitlesAsync(Document document, CancellationToken cancellationToken)
        {
            if (_lazyGetAvailablePromptTitlesAsyncDelegate.Value is null)
                return [];
 
            return await _lazyGetAvailablePromptTitlesAsyncDelegate.Value(document, cancellationToken).ConfigureAwait(false);
        }
 
        public async Task<ImmutableArray<Diagnostic>> AnalyzeDocumentAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken)
        {
            if (_lazyAnalyzeDocumentAsyncDelegate.Value is null)
                return [];
 
            return await _lazyAnalyzeDocumentAsyncDelegate.Value(document, span, promptTitle, cancellationToken).ConfigureAwait(false);
        }
 
        public async Task<ImmutableArray<Diagnostic>> GetCachedDiagnosticsAsync(Document document, string promptTitle, CancellationToken cancellationToken)
        {
            if (_lazyGetCachedDiagnosticsAsyncDelegate.Value is null)
                return [];
 
            return await _lazyGetCachedDiagnosticsAsyncDelegate.Value(document, promptTitle, cancellationToken).ConfigureAwait(false);
        }
 
        public Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken)
        {
            if (_lazyStartRefinementSessionAsyncDelegate.Value is null)
                return Task.CompletedTask;
 
            return _lazyStartRefinementSessionAsyncDelegate.Value(oldDocument, newDocument, primaryDiagnostic, cancellationToken);
        }
 
        public async Task<string> GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray<string> declarationCode, string language, CancellationToken cancellationToken)
        {
            if (_lazyGetOnTheFlyDocsAsyncDelegate.Value is null)
                return string.Empty;
 
            return await _lazyGetOnTheFlyDocsAsyncDelegate.Value(symbolSignature, declarationCode, language, cancellationToken).ConfigureAwait(false);
        }
 
        public async Task<bool> IsFileExcludedAsync(string filePath, CancellationToken cancellationToken)
        {
            if (_lazyIsFileExcludedAsyncDelegate.Value is null)
                return false;
 
            return await _lazyIsFileExcludedAsyncDelegate.Value(filePath, cancellationToken).ConfigureAwait(false);
        }
    }
}