|
// 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.ComponentModel.Composition;
using System.ComponentModel.Design;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes.Configuration;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Implementation;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.LanguageServices.Implementation.Suppression;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using Task = System.Threading.Tasks.Task;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
[Export(typeof(VisualStudioDiagnosticListTableCommandHandler))]
internal partial class VisualStudioDiagnosticListTableCommandHandler
{
private readonly IThreadingContext _threadingContext;
private readonly VisualStudioWorkspace _workspace;
private readonly VisualStudioSuppressionFixService _suppressionFixService;
private readonly VisualStudioDiagnosticListSuppressionStateService _suppressionStateService;
private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
private readonly IDiagnosticAnalyzerService _diagnosticService;
private readonly ICodeActionEditHandlerService _editHandlerService;
private readonly IAsynchronousOperationListener _listener;
private IWpfTableControl? _tableControl;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VisualStudioDiagnosticListTableCommandHandler(
IThreadingContext threadingContext,
SVsServiceProvider serviceProvider,
VisualStudioWorkspace workspace,
IVisualStudioSuppressionFixService suppressionFixService,
VisualStudioDiagnosticListSuppressionStateService suppressionStateService,
IUIThreadOperationExecutor uiThreadOperationExecutor,
IDiagnosticAnalyzerService diagnosticService,
ICodeActionEditHandlerService editHandlerService,
IAsynchronousOperationListenerProvider listenerProvider)
{
_threadingContext = threadingContext;
_workspace = workspace;
_suppressionFixService = (VisualStudioSuppressionFixService)suppressionFixService;
_suppressionStateService = suppressionStateService;
_uiThreadOperationExecutor = uiThreadOperationExecutor;
_diagnosticService = diagnosticService;
_editHandlerService = editHandlerService;
_listener = listenerProvider.GetListener(FeatureAttribute.ErrorList);
}
public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var errorList = await serviceProvider.GetServiceAsync<SVsErrorList, IErrorList>(_threadingContext.JoinableTaskFactory, throwOnFailure: false).ConfigureAwait(false);
_tableControl = errorList?.TableControl;
// Add command handlers for bulk suppression commands.
var menuCommandService = await serviceProvider.GetServiceAsync<IMenuCommandService, IMenuCommandService>(_threadingContext.JoinableTaskFactory, throwOnFailure: false).ConfigureAwait(false);
if (menuCommandService != null)
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
AddErrorListSetSeverityMenuHandlers(menuCommandService);
// The Add/Remove suppression(s) have been moved to the VS code analysis layer, so we don't add the commands here.
// TODO: Figure out how to access menu commands registered by CodeAnalysisPackage and
// add the commands here if we cannot find the new command(s) in the code analysis layer.
// AddSuppressionsCommandHandlers(menuCommandService);
}
}
private void AddErrorListSetSeverityMenuHandlers(IMenuCommandService menuCommandService)
{
Contract.ThrowIfFalse(_threadingContext.HasMainThread);
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeveritySubMenu, delegate { }, OnErrorListSetSeveritySubMenuStatus);
// Severity menu items
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityDefault, SetSeverityHandler, delegate { });
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityError, SetSeverityHandler, delegate { });
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityWarning, SetSeverityHandler, delegate { });
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityInfo, SetSeverityHandler, delegate { });
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityHidden, SetSeverityHandler, delegate { });
AddCommand(menuCommandService, ID.RoslynCommands.ErrorListSetSeverityNone, SetSeverityHandler, delegate { });
}
/// <summary>
/// Add a command handler and status query handler for a menu item
/// </summary>
private static OleMenuCommand AddCommand(
IMenuCommandService menuCommandService,
int commandId,
EventHandler invokeHandler,
EventHandler beforeQueryStatus)
{
var commandIdWithGroupId = new CommandID(Guids.RoslynGroupId, commandId);
var command = new OleMenuCommand(invokeHandler, delegate { }, beforeQueryStatus, commandIdWithGroupId);
menuCommandService.AddCommand(command);
return command;
}
private void OnAddSuppressionsStatus(object sender, EventArgs e)
{
var command = (MenuCommand)sender;
command.Visible = _suppressionStateService.CanSuppressSelectedEntries;
command.Enabled = command.Visible && !KnownUIContexts.SolutionBuildingContext.IsActive;
}
private void OnRemoveSuppressionsStatus(object sender, EventArgs e)
{
var command = (MenuCommand)sender;
command.Visible = _suppressionStateService.CanRemoveSuppressionsSelectedEntries;
command.Enabled = command.Visible && !KnownUIContexts.SolutionBuildingContext.IsActive;
}
private void OnAddSuppressionsInSourceStatus(object sender, EventArgs e)
{
var command = (MenuCommand)sender;
command.Visible = _suppressionStateService.CanSuppressSelectedEntriesInSource;
command.Enabled = command.Visible && !KnownUIContexts.SolutionBuildingContext.IsActive;
}
private void OnAddSuppressionsInSuppressionFileStatus(object sender, EventArgs e)
{
var command = (MenuCommand)sender;
command.Visible = _suppressionStateService.CanSuppressSelectedEntriesInSuppressionFiles;
command.Enabled = command.Visible && !KnownUIContexts.SolutionBuildingContext.IsActive;
}
private void OnAddSuppressionsInSource(object sender, EventArgs e)
=> _suppressionFixService.AddSuppressions(selectedErrorListEntriesOnly: true, suppressInSource: true, projectHierarchy: null);
private void OnAddSuppressionsInSuppressionFile(object sender, EventArgs e)
=> _suppressionFixService.AddSuppressions(selectedErrorListEntriesOnly: true, suppressInSource: false, projectHierarchy: null);
private void OnRemoveSuppressions(object sender, EventArgs e)
=> _suppressionFixService.RemoveSuppressions(selectedErrorListEntriesOnly: true, projectHierarchy: null);
private void OnErrorListSetSeveritySubMenuStatus(object sender, EventArgs e)
{
// For now, we only enable the Set severity menu when a single configurable diagnostic is selected in the error list
// and we can update/create an editorconfig file for the configuration entry.
// In future, we can enable support for configuring in presence of multi-selection.
var command = (MenuCommand)sender;
var selectedEntry = TryGetSingleSelectedEntry();
command.Visible = selectedEntry != null &&
!SuppressionHelpers.IsNotConfigurableDiagnostic(selectedEntry) &&
TryGetPathToAnalyzerConfigDoc(selectedEntry, out _, out _);
command.Enabled = command.Visible && !KnownUIContexts.SolutionBuildingContext.IsActive;
}
private void SetSeverityHandler(object sender, EventArgs args)
{
var selectedItem = (MenuCommand)sender;
var reportDiagnostic = TryMapSelectedItemToReportDiagnostic(selectedItem);
if (reportDiagnostic == null)
{
return;
}
var selectedDiagnostic = TryGetSingleSelectedEntry();
if (selectedDiagnostic == null)
{
return;
}
if (TryGetPathToAnalyzerConfigDoc(selectedDiagnostic, out var project, out _))
{
// Fire and forget.
_ = SetSeverityHandlerAsync(reportDiagnostic.Value, selectedDiagnostic, project);
}
}
private async Task SetSeverityHandlerAsync(ReportDiagnostic reportDiagnostic, DiagnosticData selectedDiagnostic, Project project)
{
try
{
using var token = _listener.BeginAsyncOperation(nameof(SetSeverityHandlerAsync));
using var context = _uiThreadOperationExecutor.BeginExecute(
title: ServicesVSResources.Updating_severity,
defaultDescription: ServicesVSResources.Updating_severity,
allowCancellation: true,
showProgress: true);
var newSolution = await ConfigureSeverityAsync(context.UserCancellationToken).ConfigureAwait(false);
var operations = ImmutableArray.Create<CodeActionOperation>(new ApplyChangesOperation(newSolution));
using var scope = context.AddScope(allowCancellation: true, ServicesVSResources.Updating_severity);
await _editHandlerService.ApplyAsync(
_workspace,
project.Solution,
fromDocument: null,
operations: operations,
title: ServicesVSResources.Updating_severity,
progressTracker: new UIThreadOperationContextProgressTracker(scope),
cancellationToken: context.UserCancellationToken).ConfigureAwait(false);
if (selectedDiagnostic.DocumentId != null)
{
// Kick off diagnostic re-analysis for affected document so that the configured diagnostic gets refreshed.
_ = Task.Run(() =>
{
_diagnosticService.Reanalyze(_workspace, documentIds: SpecializedCollections.SingletonEnumerable(selectedDiagnostic.DocumentId), highPriority: true);
});
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex) when (FatalError.ReportAndCatch(ex))
{
}
return;
// Local functions.
async System.Threading.Tasks.Task<Solution> ConfigureSeverityAsync(CancellationToken cancellationToken)
{
var diagnostic = await selectedDiagnostic.ToDiagnosticAsync(project, cancellationToken).ConfigureAwait(false);
return await ConfigurationUpdater.ConfigureSeverityAsync(reportDiagnostic, diagnostic, project, cancellationToken).ConfigureAwait(false);
}
}
private DiagnosticData? TryGetSingleSelectedEntry()
{
if (_tableControl?.SelectedEntries.Count() != 1)
{
return null;
}
if (!_tableControl.SelectedEntry.TryGetSnapshot(out var snapshot, out var index) ||
snapshot is not AbstractTableEntriesSnapshot<DiagnosticTableItem> roslynSnapshot)
{
return null;
}
return roslynSnapshot.GetItem(index)?.Data;
}
private bool TryGetPathToAnalyzerConfigDoc(DiagnosticData selectedDiagnostic, [NotNullWhen(true)] out Project? project, [NotNullWhen(true)] out string? pathToAnalyzerConfigDoc)
{
project = _workspace.CurrentSolution.GetProject(selectedDiagnostic.ProjectId);
pathToAnalyzerConfigDoc = project?.TryGetAnalyzerConfigPathForProjectConfiguration();
return pathToAnalyzerConfigDoc is not null;
}
private static ReportDiagnostic? TryMapSelectedItemToReportDiagnostic(MenuCommand selectedItem)
{
if (selectedItem.CommandID.Guid == Guids.RoslynGroupId)
{
return selectedItem.CommandID.ID switch
{
ID.RoslynCommands.ErrorListSetSeverityDefault => ReportDiagnostic.Default,
ID.RoslynCommands.ErrorListSetSeverityError => ReportDiagnostic.Error,
ID.RoslynCommands.ErrorListSetSeverityWarning => ReportDiagnostic.Warn,
ID.RoslynCommands.ErrorListSetSeverityInfo => ReportDiagnostic.Info,
ID.RoslynCommands.ErrorListSetSeverityHidden => ReportDiagnostic.Hidden,
ID.RoslynCommands.ErrorListSetSeverityNone => ReportDiagnostic.Suppress,
_ => (ReportDiagnostic?)null
};
}
return null;
}
}
}
|