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

Wrap calls to CompletionProvider extensions such that they go through our extension manager #70843

Merged
merged 11 commits into from
Nov 16, 2023
162 changes: 78 additions & 84 deletions src/EditorFeatures/Core/Editor/EditorLayerExtensionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,127 +4,121 @@

Copy link
Member Author

Choose a reason for hiding this comment

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

view with whitespace off.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editor.Options;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.ErrorLogger;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.Internal.Log.FunctionId;
using static Microsoft.CodeAnalysis.Internal.Log.Logger;
using static Microsoft.CodeAnalysis.RoslynAssemblyHelper;

namespace Microsoft.CodeAnalysis.Editor
namespace Microsoft.CodeAnalysis.Editor;

[ExportWorkspaceServiceFactory(typeof(IExtensionManager), ServiceLayer.Editor), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class EditorLayerExtensionManager(
[ImportMany] IEnumerable<IExtensionErrorHandler> errorHandlers) : IWorkspaceServiceFactory
{
[ExportWorkspaceServiceFactory(typeof(IExtensionManager), ServiceLayer.Editor), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class EditorLayerExtensionManager(
IGlobalOptionService optionService,
[ImportMany] IEnumerable<IExtensionErrorHandler> errorHandlers) : IWorkspaceServiceFactory
{
private readonly List<IExtensionErrorHandler> _errorHandlers = errorHandlers.ToList();
private readonly IGlobalOptionService _optionService = optionService;
private readonly ImmutableArray<IExtensionErrorHandler> _errorHandlers = errorHandlers.ToImmutableArray();

public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
var errorReportingService = workspaceServices.GetRequiredService<IErrorReportingService>();
var errorLoggerService = workspaceServices.GetRequiredService<IErrorLoggerService>();
return new ExtensionManager(_optionService, errorReportingService, errorLoggerService, _errorHandlers);
}
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
var errorReportingService = workspaceServices.GetRequiredService<IErrorReportingService>();
var errorLoggerService = workspaceServices.GetRequiredService<IErrorLoggerService>();
return new ExtensionManager(errorReportingService, errorLoggerService, _errorHandlers);
}

internal class ExtensionManager(
IGlobalOptionService globalOptions,
IErrorReportingService errorReportingService,
IErrorLoggerService errorLoggerService,
List<IExtensionErrorHandler> errorHandlers) : AbstractExtensionManager
internal sealed class ExtensionManager(
IErrorReportingService errorReportingService,
IErrorLoggerService errorLoggerService,
ImmutableArray<IExtensionErrorHandler> errorHandlers) : AbstractExtensionManager
{
protected override void HandleNonCancellationException(object provider, Exception exception)
{
private readonly List<IExtensionErrorHandler> _errorHandlers = errorHandlers;
private readonly IGlobalOptionService _globalOptions = globalOptions;
private readonly IErrorReportingService _errorReportingService = errorReportingService;
private readonly IErrorLoggerService _errorLoggerService = errorLoggerService;
Debug.Assert(exception is not OperationCanceledException);

public override void HandleException(object provider, Exception exception)
if (provider is CodeFixProvider
or CodeRefactoringProvider
Copy link
Member

Choose a reason for hiding this comment

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

Add QuickInfoProvider too?

or CodeRefactorings.FixAllProvider
or CodeFixes.FixAllProvider
or CompletionProvider)
Copy link
Member Author

Choose a reason for hiding this comment

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

added CompletionProvider here. That way this participates in teh yellow-bar, telling the user about the completion provider that thrw the exception and if htey want to enable it.

{
if (provider is CodeFixProvider or CodeFixes.FixAllProvider or CodeRefactoringProvider or CodeRefactorings.FixAllProvider)
if (!IsIgnored(provider))
{
if (!IsIgnored(provider) &&
_globalOptions.GetOption(ExtensionManagerOptions.DisableCrashingExtensions))
Copy link
Member Author

Choose a reason for hiding this comment

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

this option was always true, and had no way to configure it. removed it.

{
base.HandleException(provider, exception);

var providerType = provider.GetType();

_errorReportingService?.ShowGlobalErrorInfo(
message: string.Format(WorkspacesResources._0_encountered_an_error_and_has_been_disabled, providerType.Name),
TelemetryFeatureName.GetExtensionName(providerType),
exception,
new InfoBarUI(WorkspacesResources.Show_Stack_Trace, InfoBarUI.UIKind.HyperLink, () => ShowDetailedErrorInfo(exception), closeAfterAction: false),
new InfoBarUI(WorkspacesResources.Enable, InfoBarUI.UIKind.Button, () =>
{
EnableProvider(provider);
LogEnableProvider(provider);
}),
new InfoBarUI(WorkspacesResources.Enable_and_ignore_future_errors, InfoBarUI.UIKind.Button, () =>
{
EnableProvider(provider);
IgnoreProvider(provider);
LogEnableAndIgnoreProvider(provider);
}),
new InfoBarUI(string.Empty, InfoBarUI.UIKind.Close, () => LogLeaveDisabled(provider)));
}
else
{
LogAction(CodefixInfobar_ErrorIgnored, provider);
}
this.DisableProvider(provider);

var providerType = provider.GetType();

errorReportingService?.ShowGlobalErrorInfo(
message: string.Format(WorkspacesResources._0_encountered_an_error_and_has_been_disabled, providerType.Name),
TelemetryFeatureName.GetExtensionName(providerType),
exception,
new InfoBarUI(WorkspacesResources.Show_Stack_Trace, InfoBarUI.UIKind.HyperLink, () => ShowDetailedErrorInfo(exception), closeAfterAction: false),
new InfoBarUI(WorkspacesResources.Enable, InfoBarUI.UIKind.Button, () =>
{
EnableProvider(provider);
LogEnableProvider(provider);
}),
new InfoBarUI(WorkspacesResources.Enable_and_ignore_future_errors, InfoBarUI.UIKind.Button, () =>
{
EnableProvider(provider);
IgnoreProvider(provider);
LogEnableAndIgnoreProvider(provider);
}),
new InfoBarUI(string.Empty, InfoBarUI.UIKind.Close, () => LogLeaveDisabled(provider)));
}
else
{
if (_globalOptions.GetOption(ExtensionManagerOptions.DisableCrashingExtensions))
{
base.HandleException(provider, exception);
}

_errorHandlers.Do(h => h.HandleError(provider, exception));
LogAction(CodefixInfobar_ErrorIgnored, provider);
}
}
else
{

_errorLoggerService?.LogException(provider, exception);
this.DisableProvider(provider);
errorHandlers.Do(h => h.HandleError(provider, exception));
}

private void ShowDetailedErrorInfo(Exception exception)
=> _errorReportingService.ShowDetailedErrorInfo(exception);
errorLoggerService?.LogException(provider, exception);
}

private void ShowDetailedErrorInfo(Exception exception)
=> errorReportingService.ShowDetailedErrorInfo(exception);

private static void LogLeaveDisabled(object provider)
=> LogAction(CodefixInfobar_LeaveDisabled, provider);
private static void LogLeaveDisabled(object provider)
=> LogAction(CodefixInfobar_LeaveDisabled, provider);

private static void LogEnableAndIgnoreProvider(object provider)
=> LogAction(CodefixInfobar_EnableAndIgnoreFutureErrors, provider);
private static void LogEnableAndIgnoreProvider(object provider)
=> LogAction(CodefixInfobar_EnableAndIgnoreFutureErrors, provider);

private static void LogEnableProvider(object provider)
=> LogAction(CodefixInfobar_Enable, provider);
private static void LogEnableProvider(object provider)
=> LogAction(CodefixInfobar_Enable, provider);

private static void LogAction(FunctionId functionId, object provider)
private static void LogAction(FunctionId functionId, object provider)
{
if (IsRoslynCodefix(provider))
{
if (IsRoslynCodefix(provider))
{
Log(functionId, $"Name: {provider.GetType().FullName} Assembly Version: {provider.GetType().Assembly.GetName().Version}");
}
else
{
Log(functionId);
}
Log(functionId, $"Name: {provider.GetType().FullName} Assembly Version: {provider.GetType().Assembly.GetName().Version}");
}
else
{
Log(functionId);
}

private static bool IsRoslynCodefix(object source) => HasRoslynPublicKey(source);
}

private static bool IsRoslynCodefix(object source) => HasRoslynPublicKey(source);
}
}
14 changes: 0 additions & 14 deletions src/EditorFeatures/Core/Options/ExtensionManagerOptions.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public async Task<ImmutableArray<CodeRefactoring>> GetRefactoringsAsync(
}
}

private async Task<CodeRefactoring?> GetRefactoringFromProviderAsync(
private Task<CodeRefactoring?> GetRefactoringFromProviderAsync(
TextDocument textDocument,
TextSpan state,
CodeRefactoringProvider provider,
Expand All @@ -169,58 +169,43 @@ public async Task<ImmutableArray<CodeRefactoring>> GetRefactoringsAsync(
CodeActionOptionsProvider options,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (extensionManager.IsDisabled(provider))
{
return null;
}

try
{
using var _ = ArrayBuilder<(CodeAction action, TextSpan? applicableToSpan)>.GetInstance(out var actions);
var context = new CodeRefactoringContext(textDocument, state,

// TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs?
(action, applicableToSpan) =>
{
// Serialize access for thread safety - we don't know what thread the refactoring provider will call this delegate from.
lock (actions)
{
// Add the Refactoring Provider Name to the parent CodeAction's CustomTags.
// Always add a name even in cases of 3rd party refactorings that do not export
// name metadata.
action.AddCustomTagAndTelemetryInfo(providerMetadata, provider);

actions.Add((action, applicableToSpan));
}
},
options,
cancellationToken);

var task = provider.ComputeRefactoringsAsync(context) ?? Task.CompletedTask;
await task.ConfigureAwait(false);

if (actions.Count == 0)
return extensionManager.PerformFunctionAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

moved all explicit try/catch code with extension manager to use the existing helper functions that already do the right thing.

provider,
async () =>
{
return null;
}
cancellationToken.ThrowIfCancellationRequested();
using var _ = ArrayBuilder<(CodeAction action, TextSpan? applicableToSpan)>.GetInstance(out var actions);
var context = new CodeRefactoringContext(textDocument, state,

var fixAllProviderInfo = extensionManager.PerformFunction(
provider, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, provider, FixAllProviderInfo.Create), defaultValue: null);
return new CodeRefactoring(provider, actions.ToImmutable(), fixAllProviderInfo, options);
}
catch (OperationCanceledException)
{
// We don't want to catch operation canceled exceptions in the catch block
// below. So catch is here and rethrow it.
throw;
}
catch (Exception e)
{
extensionManager.HandleException(provider, e);
}
// TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs?
(action, applicableToSpan) =>
{
// Serialize access for thread safety - we don't know what thread the refactoring provider will call this delegate from.
lock (actions)
{
// Add the Refactoring Provider Name to the parent CodeAction's CustomTags.
// Always add a name even in cases of 3rd party refactorings that do not export
// name metadata.
action.AddCustomTagAndTelemetryInfo(providerMetadata, provider);

actions.Add((action, applicableToSpan));
}
},
options,
cancellationToken);

var task = provider.ComputeRefactoringsAsync(context) ?? Task.CompletedTask;
await task.ConfigureAwait(false);

if (actions.Count == 0)
{
return null;
}

return null;
var fixAllProviderInfo = extensionManager.PerformFunction(
provider, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, provider, FixAllProviderInfo.Create), defaultValue: null);
return new CodeRefactoring(provider, actions.ToImmutable(), fixAllProviderInfo, options);
}, defaultValue: null);
}

private class ProjectCodeRefactoringProvider
Expand Down
27 changes: 23 additions & 4 deletions src/Features/Core/Portable/Completion/CompletionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
Expand Down Expand Up @@ -154,8 +155,13 @@ internal virtual bool ShouldTriggerCompletion(
return char.IsLetterOrDigit(trigger.Character) || trigger.Character == '.';
}

var extensionManager = languageServices.SolutionServices.GetRequiredService<IExtensionManager>();

var providers = _providerManager.GetFilteredProviders(project, roles, trigger, options);
return providers.Any(p => p.ShouldTriggerCompletion(languageServices, text, caretPosition, trigger, options, passThroughOptions));
return providers.Any(p =>
extensionManager.PerformFunction(p,
() => p.ShouldTriggerCompletion(languageServices, text, caretPosition, trigger, options, passThroughOptions),
defaultValue: false));
}

/// <summary>
Expand Down Expand Up @@ -205,9 +211,14 @@ public virtual TextSpan GetDefaultCompletionListSpan(SourceText text, int caretP
if (provider is null)
return CompletionDescription.Empty;

var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService<IExtensionManager>();

// We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it.
(document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false);
var description = await provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken).ConfigureAwait(false);
var description = await extensionManager.PerformFunctionAsync(
provider,
() => provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken),
defaultValue: null).ConfigureAwait(false);
GC.KeepAlive(semanticModel);
return description;
}
Expand All @@ -230,11 +241,19 @@ public virtual async Task<CompletionChange> GetChangeAsync(
var provider = GetProvider(item, document.Project);
if (provider != null)
{
var extensionManager = document.Project.Solution.Workspace.Services.GetRequiredService<IExtensionManager>();

// We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it.
(document, var semanticModel) = await GetDocumentWithFrozenPartialSemanticsAsync(document, cancellationToken).ConfigureAwait(false);
var change = await provider.GetChangeAsync(document, item, commitCharacter, cancellationToken).ConfigureAwait(false);
GC.KeepAlive(semanticModel);

var change = await extensionManager.PerformFunctionAsync(
provider,
() => provider.GetChangeAsync(document, item, commitCharacter, cancellationToken),
defaultValue: null!).ConfigureAwait(false);
if (change == null)
return CompletionChange.Create(new TextChange(new TextSpan(), ""));

GC.KeepAlive(semanticModel);
Debug.Assert(item.Span == change.TextChange.Span || item.IsComplexTextEdit);
return change;
}
Expand Down
Loading
Loading