diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/FixAllItem.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/FixAllItem.cs new file mode 100644 index 0000000000..722819ce04 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/FixAllItem.cs @@ -0,0 +1,15 @@ +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + public class FixAllItem + { + public FixAllItem() {} + public FixAllItem(string id, string message) + { + Id = id; + Message = message; + } + + public string Id { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllRequest.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllRequest.cs new file mode 100644 index 0000000000..a6b4efba29 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllRequest.cs @@ -0,0 +1,11 @@ +using OmniSharp.Mef; +using OmniSharp.Models; + +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + [OmniSharpEndpoint(OmniSharpEndpoints.GetFixAll, typeof(GetFixAllRequest), typeof(GetFixAllResponse))] + public class GetFixAllRequest: SimpleFileRequest + { + public FixAllScope Scope { get; set; } = FixAllScope.Document; + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllResponse.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllResponse.cs new file mode 100644 index 0000000000..70e253e428 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/GetFixAllResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + public class GetFixAllResponse + { + public GetFixAllResponse(IEnumerable fixableItems) + { + Items = fixableItems; + } + + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllRequest.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllRequest.cs new file mode 100644 index 0000000000..aef5e65cd4 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllRequest.cs @@ -0,0 +1,17 @@ +using OmniSharp.Mef; +using OmniSharp.Models; + +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + [OmniSharpEndpoint(OmniSharpEndpoints.RunFixAll, typeof(RunFixAllRequest), typeof(RunFixAllResponse))] + public class RunFixAllRequest : SimpleFileRequest + { + public FixAllScope Scope { get; set; } = FixAllScope.Document; + + // If this is null -> filter not set -> try to fix all issues in current defined scope. + public FixAllItem[] FixAllFilter { get; set; } + public int Timeout { get; set; } = 3000; + public bool WantsAllCodeActionOperations { get; set; } + public bool WantsTextChanges { get; set; } + } +} diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllResponse.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllResponse.cs new file mode 100644 index 0000000000..b5b00fd9bd --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using OmniSharp.Models; + +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + public class RunFixAllResponse : IAggregateResponse + { + public RunFixAllResponse() + { + Changes = new List(); + } + + public IEnumerable Changes { get; set; } + + public IAggregateResponse Merge(IAggregateResponse response) { return response; } + } +} diff --git a/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllScope.cs b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllScope.cs new file mode 100644 index 0000000000..5b7adbc3a6 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/FixAll/RunFixAllScope.cs @@ -0,0 +1,9 @@ +namespace OmniSharp.Abstractions.Models.V1.FixAll +{ + public enum FixAllScope + { + Document = 0, + Project = 1, + Solution = 2 + } +} diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index 141768c42e..b20b5c3787 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -34,7 +34,8 @@ public static class OmniSharpEndpoints public const string WorkspaceInformation = "/projects"; public const string ProjectInformation = "/project"; public const string FixUsings = "/fixusings"; - + public const string RunFixAll = "/runfixall"; + public const string GetFixAll = "/getfixall"; public const string CheckAliveStatus = "/checkalivestatus"; public const string CheckReadyStatus = "/checkreadystatus"; public const string StopServer = "/stopserver"; diff --git a/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs index a8e713e293..183c60b8fb 100644 --- a/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs +++ b/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs @@ -1,16 +1,16 @@ using Microsoft.CodeAnalysis; using OmniSharp.Models.Diagnostics; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading.Tasks; namespace OmniSharp.Helpers { internal static class DiagnosticExtensions { private static readonly ImmutableHashSet _tagFilter = - ImmutableHashSet.Create("Unnecessary"); + ImmutableHashSet.Create("Unnecessary"); internal static DiagnosticLocation ToDiagnosticLocation(this Diagnostic diagnostic) { @@ -32,9 +32,10 @@ internal static DiagnosticLocation ToDiagnosticLocation(this Diagnostic diagnost }; } - internal static IEnumerable DistinctDiagnosticLocationsByProject(this IEnumerable<(string projectName, Diagnostic diagnostic)> diagnostics) + internal static IEnumerable DistinctDiagnosticLocationsByProject(this IEnumerable documentDiagnostic) { - return diagnostics + return documentDiagnostic + .SelectMany(x => x.Diagnostics, (parent, child) => (projectName: parent.ProjectName, diagnostic: child)) .Select(x => new { location = x.diagnostic.ToDiagnosticLocation(), diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs index e0e526921d..2c8ec78556 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading.Tasks; @@ -9,7 +8,6 @@ using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.CodeCheck; -using OmniSharp.Models.Diagnostics; using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; @@ -45,11 +43,11 @@ public async Task Handle(CodeCheckRequest request) return GetResponseFromDiagnostics(diagnostics, request.FileName); } - private static QuickFixResponse GetResponseFromDiagnostics(ImmutableArray<(string projectName, Diagnostic diagnostic)> diagnostics, string fileName) + private static QuickFixResponse GetResponseFromDiagnostics(ImmutableArray diagnostics, string fileName) { var diagnosticLocations = diagnostics - .Where(x => (string.IsNullOrEmpty(fileName) - || x.diagnostic.Location.GetLineSpan().Path == fileName)) + .Where(x => string.IsNullOrEmpty(fileName) + || x.DocumentPath == fileName) .DistinctDiagnosticLocationsByProject() .Where(x => x.FileName != null); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/DocumentWithFixProvidersAndMatchingDiagnostics.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/DocumentWithFixProvidersAndMatchingDiagnostics.cs new file mode 100644 index 0000000000..ab51ee46d5 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/DocumentWithFixProvidersAndMatchingDiagnostics.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; + +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring +{ + + public class DocumentWithFixProvidersAndMatchingDiagnostics + { + private readonly DocumentDiagnostics _documentDiagnostics; + + // http://source.roslyn.io/#Microsoft.VisualStudio.LanguageServices.CSharp/LanguageService/CSharpCodeCleanupFixer.cs,d9a375db0f1e430e,references + // CS8019 isn't directly used (via roslyn) but has an analyzer that report different diagnostic based on CS8019 to improve user experience. + private static readonly Dictionary _customDiagVsFixMap = new Dictionary + { + { "CS8019", "RemoveUnnecessaryImportsFixable" } + }; + + private DocumentWithFixProvidersAndMatchingDiagnostics(CodeFixProvider provider, DocumentDiagnostics documentDiagnostics) + { + CodeFixProvider = provider; + _documentDiagnostics = documentDiagnostics; + FixAllProvider = provider.GetFixAllProvider(); + } + + public CodeFixProvider CodeFixProvider { get; } + public FixAllProvider FixAllProvider { get; } + public DocumentId DocumentId => _documentDiagnostics.DocumentId; + public ProjectId ProjectId => _documentDiagnostics.ProjectId; + public string DocumentPath => _documentDiagnostics.DocumentPath; + + public IEnumerable<(string id, string messsage)> FixableDiagnostics => _documentDiagnostics.Diagnostics + .Where(x => HasFixToShow(x.Id)) + .Select(x => (x.Id, x.GetMessage())); + + // Theres specific filterings between what is shown and what is fixed because of some custom mappings + // between diagnostics and their fixers. We dont want to show text 'RemoveUnnecessaryImportsFixable: ...' + // but instead 'CS8019: ...' where actual fixer is RemoveUnnecessaryImportsFixable behind the scenes. + private bool HasFixToShow(string diagnosticId) + { + return CodeFixProvider.FixableDiagnosticIds.Any(id => id == diagnosticId) && !_customDiagVsFixMap.ContainsValue(diagnosticId) || HasMappedFixAvailable(diagnosticId); + } + + public bool HasFixForId(string diagnosticId) + { + return CodeFixProvider.FixableDiagnosticIds.Any(id => id == diagnosticId && !_customDiagVsFixMap.ContainsKey(diagnosticId)) || HasMappedFixAvailable(diagnosticId); + } + + private bool HasMappedFixAvailable(string diagnosticId) + { + return (_customDiagVsFixMap.ContainsKey(diagnosticId) && CodeFixProvider.FixableDiagnosticIds.Any(id => id == _customDiagVsFixMap[diagnosticId])); + } + + public static ImmutableArray CreateWithMatchingProviders(ImmutableArray providers, DocumentDiagnostics documentDiagnostics) + { + return + providers + .Select(provider => new DocumentWithFixProvidersAndMatchingDiagnostics(provider, documentDiagnostics)) + .Where(x => x._documentDiagnostics.Diagnostics.Any(d => x.HasFixToShow(d.Id)) || x._documentDiagnostics.Diagnostics.Any(d => x.HasFixForId(d.Id))) + .Where(x => x.FixAllProvider != null) + .ToImmutableArray(); + } + + public async Task<(CodeAction action, ImmutableArray idsToFix)> RegisterCodeFixesOrDefault(Document document) + { + CodeAction action = null; + + var fixableDiagnostics = _documentDiagnostics + .Diagnostics + .Where(x => HasFixForId(x.Id)) + .ToImmutableArray(); + + foreach (var diagnostic in fixableDiagnostics) + { + var context = new CodeFixContext( + document, + diagnostic, + (a, _) => + { + if (action == null) + { + action = a; + } + }, + CancellationToken.None); + + await CodeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); + } + + if(action == null) + return default; + + return (action, fixableDiagnostics.Select(x => x.Id).Distinct().ToImmutableArray()); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/GetFixAllCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/GetFixAllCodeActionService.cs new file mode 100644 index 0000000000..24d516b324 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/GetFixAllCodeActionService.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using OmniSharp.Abstractions.Models.V1.FixAll; +using OmniSharp.Mef; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Services; + +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring +{ + [OmniSharpHandler(OmniSharpEndpoints.GetFixAll, LanguageNames.CSharp)] + public class GetFixAllCodeActionService : BaseCodeActionService + { + [ImportingConstructor] + public GetFixAllCodeActionService( + OmniSharpWorkspace workspace, + [ImportMany] IEnumerable providers, + ILoggerFactory loggerFactory, + ICsDiagnosticWorker diagnostics, + CachingCodeFixProviderForProjects codeFixesForProject + ) : base(workspace, providers, loggerFactory.CreateLogger(), diagnostics, codeFixesForProject) + { + } + + public override async Task Handle(GetFixAllRequest request) + { + var availableFixes = await GetDiagnosticsMappedWithFixAllProviders(request.Scope, request.FileName); + + var distinctDiagnosticsThatCanBeFixed = availableFixes + .SelectMany(x => x.FixableDiagnostics) + .GroupBy(x => x.id) // Distinct isn't good fit here since theres cases where Id has multiple different messages based on location, just show one of them. + .Select(x => x.First()) + .Select(x => new FixAllItem(x.id, x.messsage)) + .OrderBy(x => x.Id) + .ToArray(); + + return new GetFixAllResponse(distinctDiagnosticsThatCanBeFixed); + } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/RunFixAllCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/RunFixAllCodeActionService.cs new file mode 100644 index 0000000000..1bde6dd6a7 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/RunFixAllCodeActionService.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.Extensions.Logging; +using OmniSharp.Abstractions.Models.V1.FixAll; +using OmniSharp.Mef; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Services; +using FixAllScope = OmniSharp.Abstractions.Models.V1.FixAll.FixAllScope; + +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring +{ + [OmniSharpHandler(OmniSharpEndpoints.RunFixAll, LanguageNames.CSharp)] + public class RunFixAllCodeActionService : BaseCodeActionService + { + private readonly ILogger _logger; + private readonly FixAllDiagnosticProvider _fixAllDiagnosticProvider; + + [ImportingConstructor] + public RunFixAllCodeActionService(ICsDiagnosticWorker diagnosticWorker, + [ImportMany] IEnumerable providers, + CachingCodeFixProviderForProjects codeFixProvider, + OmniSharpWorkspace workspace, + ILoggerFactory loggerFactory) : + base( + workspace, + providers, + loggerFactory.CreateLogger(), + diagnosticWorker, + codeFixProvider) + { + _logger = loggerFactory.CreateLogger(); + _fixAllDiagnosticProvider = new FixAllDiagnosticProvider(diagnosticWorker); + } + + public async override Task Handle(RunFixAllRequest request) + { + if (request.Scope != FixAllScope.Document && request.FixAllFilter == null) + throw new NotImplementedException($"Only scope '{nameof(FixAllScope.Document)}' is currently supported when filter '{nameof(request.FixAllFilter)}' is not set."); + + var solutionBeforeChanges = Workspace.CurrentSolution; + + var mappedProvidersWithDiagnostics = await GetDiagnosticsMappedWithFixAllProviders(request.Scope, request.FileName); + + var filteredProvidersWithFix = mappedProvidersWithDiagnostics + .Where(diagWithFix => + { + if (request.FixAllFilter == null) + return true; + + return request.FixAllFilter.Any(x => diagWithFix.HasFixForId(x.Id)); + }); + + var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout)); + + foreach (var singleFixableProviderWithDocument in filteredProvidersWithFix) + { + try + { + var document = Workspace.CurrentSolution.GetDocument(singleFixableProviderWithDocument.DocumentId); + + var fixer = singleFixableProviderWithDocument.FixAllProvider; + + var (action, fixableDiagnosticIds) = await singleFixableProviderWithDocument.RegisterCodeFixesOrDefault(document); + + if (action == null) + continue; + + var fixAllContext = new FixAllContext( + document, + singleFixableProviderWithDocument.CodeFixProvider, + Microsoft.CodeAnalysis.CodeFixes.FixAllScope.Project, + action.EquivalenceKey, + fixableDiagnosticIds, + _fixAllDiagnosticProvider, + cancellationSource.Token + ); + + var fixes = await singleFixableProviderWithDocument.FixAllProvider.GetFixAsync(fixAllContext); + + if (fixes == null) + continue; + + var operations = await fixes.GetOperationsAsync(cancellationSource.Token); + + foreach (var o in operations) + { + _logger.LogInformation($"Applying operation {o.ToString()} from fix all with fix provider {singleFixableProviderWithDocument.CodeFixProvider} to workspace document {document.FilePath}."); + + if (o is ApplyChangesOperation applyChangesOperation) + { + applyChangesOperation.Apply(Workspace, cancellationSource.Token); + } + } + } + catch (Exception ex) + { + _logger.LogError($"Running fix all action {singleFixableProviderWithDocument} in document {singleFixableProviderWithDocument.DocumentPath} prevented by error: {ex}"); + } + } + + var changes = await GetFileChangesAsync(Workspace.CurrentSolution, solutionBeforeChanges, Path.GetDirectoryName(request.FileName), request.WantsTextChanges, request.WantsAllCodeActionOperations); + + return new RunFixAllResponse + { + Changes = changes.FileChanges + }; + } + + private class FixAllDiagnosticProvider : FixAllContext.DiagnosticProvider + { + private readonly ICsDiagnosticWorker _diagnosticWorker; + + public FixAllDiagnosticProvider(ICsDiagnosticWorker diagnosticWorker) + { + _diagnosticWorker = diagnosticWorker; + } + + public override async Task> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + var diagnostics = await _diagnosticWorker.GetDiagnostics(project.Documents.Select(x => x.FilePath).ToImmutableArray()); + return diagnostics.SelectMany(x => x.Diagnostics); + } + + public override async Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken) + { + var documentDiagnostics = await _diagnosticWorker.GetDiagnostics(ImmutableArray.Create(document.FilePath)); + + if (!documentDiagnostics.Any()) + return new Diagnostic[] { }; + + return documentDiagnostics.First().Diagnostics; + } + + public override async Task> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + var diagnostics = await _diagnosticWorker.GetDiagnostics(project.Documents.Select(x => x.FilePath).ToImmutableArray()); + return diagnostics.SelectMany(x => x.Diagnostics); + } + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs index 5f85ccd68b..62622c71b9 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -12,14 +13,15 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Extensions; -using OmniSharp.Helpers; using OmniSharp.Mef; +using OmniSharp.Models; using OmniSharp.Models.V2.CodeActions; -using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.Diagnostics; using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using OmniSharp.Utilities; +using FixAllScope = OmniSharp.Abstractions.Models.V1.FixAll.FixAllScope; namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 { @@ -47,9 +49,9 @@ protected BaseCodeActionService( ICsDiagnosticWorker diagnostics, CachingCodeFixProviderForProjects codeFixesForProject) { - this.Workspace = workspace; - this.Providers = providers; - this.Logger = logger; + Workspace = workspace; + Providers = providers; + Logger = logger; this.diagnostics = diagnostics; this.codeFixesForProject = codeFixesForProject; OrderedCodeRefactoringProviders = new Lazy>(() => GetSortedCodeRefactoringProviders()); @@ -98,10 +100,11 @@ protected async Task> GetAvailableCodeActions(I private static IEnumerable FilterBlacklistedCodeActions(IEnumerable codeActions) { // Most of actions with UI works fine with defaults, however there's few exceptions: - return codeActions.Where(x => { + return codeActions.Where(x => + { var actionName = x.CodeAction.GetType().Name; - return actionName != "GenerateTypeCodeActionWithOption" && // Blacklisted because doesn't give additional value over non UI generate type (when defaults used.) + return actionName != "GenerateTypeCodeActionWithOption" && // Blacklisted because doesn't give additional value over non UI generate type (when defaults used.) actionName != "ChangeSignatureCodeAction" && // Blacklisted because cannot be used without proper UI. actionName != "PullMemberUpWithDialogCodeAction"; // Blacklisted because doesn't give additional value over non UI generate type (when defaults used.) }); @@ -120,10 +123,10 @@ private TextSpan GetTextSpan(ICodeActionRequest request, SourceText sourceText) private async Task CollectCodeFixesActions(Document document, TextSpan span, List codeActions) { - var diagnosticsWithProjects = await this.diagnostics.GetDiagnostics(ImmutableArray.Create(document.FilePath)); + var diagnosticsWithProjects = await diagnostics.GetDiagnostics(ImmutableArray.Create(document.FilePath)); var groupedBySpan = diagnosticsWithProjects - .Select(x => x.diagnostic) + .SelectMany(x => x.Diagnostics) .Where(diagnostic => span.IntersectsWith(diagnostic.Location.SourceSpan)) .GroupBy(diagnostic => diagnostic.Location.SourceSpan); @@ -160,11 +163,7 @@ private async Task AppendFixesAsync(Document document, TextSpan span, IEnumerabl private List GetSortedCodeFixProviders(Document document) { - var providerList = - this.Providers.SelectMany(provider => provider.CodeFixProviders) - .Concat(codeFixesForProject.GetAllCodeFixesForProject(document.Project.Id)); - - return ExtensionOrderer.GetOrderedOrUnorderedList(providerList, attribute => attribute.Name).ToList(); + return ExtensionOrderer.GetOrderedOrUnorderedList(codeFixesForProject.GetAllCodeFixesForProject(document.Project.Id), attribute => attribute.Name).ToList(); } private List GetSortedCodeRefactoringProviders() @@ -211,5 +210,153 @@ private IEnumerable ConvertToAvailableCodeAction(IEnumerabl return new[] { new AvailableCodeAction(action) }; }); } + + // Mapping means: each mapped item has one document that has one code fix provider and it's corresponding diagnostics. + // If same document has multiple codefixers (diagnostics with different fixers) will them be mapped as separate items. + protected async Task> GetDiagnosticsMappedWithFixAllProviders(FixAllScope scope, string fileName) + { + ImmutableArray allDiagnostics = await GetCorrectDiagnosticsInScope(scope, fileName); + + var mappedProvidersWithDiagnostics = allDiagnostics + .SelectMany(diagnosticsInDocument => + DocumentWithFixProvidersAndMatchingDiagnostics.CreateWithMatchingProviders(codeFixesForProject.GetAllCodeFixesForProject(diagnosticsInDocument.ProjectId), diagnosticsInDocument)); + + return mappedProvidersWithDiagnostics.ToImmutableArray(); + } + + private async Task> GetCorrectDiagnosticsInScope(FixAllScope scope, string fileName) + { + switch (scope) + { + case FixAllScope.Solution: + var documentsInSolution = Workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).Select(x => x.FilePath).ToImmutableArray(); + return await diagnostics.GetDiagnostics(documentsInSolution); + case FixAllScope.Project: + var documentsInProject = Workspace.GetDocument(fileName).Project.Documents.Select(x => x.FilePath).ToImmutableArray(); + return await diagnostics.GetDiagnostics(documentsInProject); + case FixAllScope.Document: + return await diagnostics.GetDiagnostics(ImmutableArray.Create(fileName)); + default: + throw new NotImplementedException(); + } + } + + protected async Task<(Solution Solution, IEnumerable FileChanges)> GetFileChangesAsync(Solution newSolution, Solution oldSolution, string directory, bool wantTextChanges, bool wantsAllCodeActionOperations) + { + var solution = oldSolution; + var filePathToResponseMap = new Dictionary(); + var solutionChanges = newSolution.GetChanges(oldSolution); + + foreach (var projectChange in solutionChanges.GetProjectChanges()) + { + // Handle added documents + foreach (var documentId in projectChange.GetAddedDocuments()) + { + var newDocument = newSolution.GetDocument(documentId); + var text = await newDocument.GetTextAsync(); + + var newFilePath = newDocument.FilePath == null || !Path.IsPathRooted(newDocument.FilePath) + ? Path.Combine(directory, newDocument.Name) + : newDocument.FilePath; + + var modifiedFileResponse = new ModifiedFileResponse(newFilePath) + { + Changes = new[] { + new LinePositionSpanTextChange + { + NewText = text.ToString() + } + } + }; + + filePathToResponseMap[newFilePath] = modifiedFileResponse; + + // We must add new files to the workspace to ensure that they're present when the host editor + // tries to modify them. This is a strange interaction because the workspace could be left + // in an incomplete state if the host editor doesn't apply changes to the new file, but it's + // what we've got today. + if (this.Workspace.GetDocument(newFilePath) == null) + { + var fileInfo = new FileInfo(newFilePath); + if (!fileInfo.Exists) + { + fileInfo.CreateText().Dispose(); + } + else + { + // The file already exists on disk? Ensure that it's zero-length. If so, we can still use it. + if (fileInfo.Length > 0) + { + Logger.LogError($"File already exists on disk: '{newFilePath}'"); + break; + } + } + + this.Workspace.AddDocument(documentId, projectChange.NewProject, newFilePath, newDocument.SourceCodeKind); + solution = this.Workspace.CurrentSolution; + } + else + { + // The file already exists in the workspace? We're in a bad state. + Logger.LogError($"File already exists in workspace: '{newFilePath}'"); + } + } + + // Handle changed documents + foreach (var documentId in projectChange.GetChangedDocuments()) + { + var newDocument = newSolution.GetDocument(documentId); + var oldDocument = oldSolution.GetDocument(documentId); + var filePath = newDocument.FilePath; + + // file rename + if (oldDocument != null && newDocument.Name != oldDocument.Name) + { + if (wantsAllCodeActionOperations) + { + var newFilePath = GetNewFilePath(newDocument.Name, oldDocument.FilePath); + var text = await oldDocument.GetTextAsync(); + var temp = solution.RemoveDocument(documentId); + solution = temp.AddDocument(DocumentId.CreateNewId(oldDocument.Project.Id, newDocument.Name), newDocument.Name, text, oldDocument.Folders, newFilePath); + + filePathToResponseMap[filePath] = new RenamedFileResponse(oldDocument.FilePath, newFilePath); + filePathToResponseMap[newFilePath] = new OpenFileResponse(newFilePath); + } + continue; + } + + if (!filePathToResponseMap.TryGetValue(filePath, out var fileOperationResponse)) + { + fileOperationResponse = new ModifiedFileResponse(filePath); + filePathToResponseMap[filePath] = fileOperationResponse; + } + + if (fileOperationResponse is ModifiedFileResponse modifiedFileResponse) + { + if (wantTextChanges) + { + var linePositionSpanTextChanges = await TextChanges.GetAsync(newDocument, oldDocument); + + modifiedFileResponse.Changes = modifiedFileResponse.Changes != null + ? modifiedFileResponse.Changes.Union(linePositionSpanTextChanges) + : linePositionSpanTextChanges; + } + else + { + var text = await newDocument.GetTextAsync(); + modifiedFileResponse.Buffer = text.ToString(); + } + } + } + } + + return (solution, filePathToResponseMap.Values); + } + + private static string GetNewFilePath(string newFileName, string currentFilePath) + { + var directory = Path.GetDirectoryName(currentFilePath); + return Path.Combine(directory, newFileName); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs index 04abe47c7b..ea230f0a16 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs @@ -18,16 +18,17 @@ namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 [Export(typeof(CachingCodeFixProviderForProjects))] public class CachingCodeFixProviderForProjects { - private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(); private readonly ILogger _logger; private readonly OmniSharpWorkspace _workspace; + private readonly IEnumerable _providers; [ImportingConstructor] - public CachingCodeFixProviderForProjects(ILoggerFactory loggerFactory, OmniSharpWorkspace workspace) + public CachingCodeFixProviderForProjects(ILoggerFactory loggerFactory, OmniSharpWorkspace workspace, [ImportMany] IEnumerable providers) { _logger = loggerFactory.CreateLogger(); _workspace = workspace; - + _providers = providers; _workspace.WorkspaceChanged += (__, workspaceEvent) => { if (workspaceEvent.Kind == WorkspaceChangeKind.ProjectAdded || @@ -40,7 +41,7 @@ public CachingCodeFixProviderForProjects(ILoggerFactory loggerFactory, OmniSharp }; } - public IEnumerable GetAllCodeFixesForProject(ProjectId projectId) + public ImmutableArray GetAllCodeFixesForProject(ProjectId projectId) { if (_cache.ContainsKey(projectId)) return _cache[projectId]; @@ -50,7 +51,7 @@ public IEnumerable GetAllCodeFixesForProject(ProjectId projectI if (project == null) { _cache.TryRemove(projectId, out _); - return Enumerable.Empty(); + return ImmutableArray.Empty; } return LoadFrom(project); @@ -58,7 +59,7 @@ public IEnumerable GetAllCodeFixesForProject(ProjectId projectI private ImmutableArray LoadFrom(Project project) { - var codeFixes = project.AnalyzerReferences + var codeFixesFromProjectReferences = project.AnalyzerReferences .OfType() .SelectMany(analyzerFileReference => analyzerFileReference.GetAssembly().DefinedTypes) .Where(x => !x.IsAbstract && x.IsSubclassOf(typeof(CodeFixProvider))) @@ -87,12 +88,15 @@ private ImmutableArray LoadFrom(Project project) return null; } }) - .Where(x => x != null) - .ToImmutableArray(); + .Where(x => x != null); + + var builtInCodeFixes = _providers.SelectMany(provider => provider.CodeFixProviders); + + var allCodeFixes = builtInCodeFixes.Concat(codeFixesFromProjectReferences).ToImmutableArray(); - _cache.AddOrUpdate(project.Id, codeFixes, (_, __) => codeFixes); + _cache.AddOrUpdate(project.Id, allCodeFixes, (_, __) => allCodeFixes); - return codeFixes; + return allCodeFixes; } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs index 5ade3b1ca8..57893a5792 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs @@ -6,9 +6,7 @@ using Microsoft.Extensions.Logging; using OmniSharp.Mef; using OmniSharp.Models.V2.CodeActions; -using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.CodeActions; -using OmniSharp.Roslyn.CSharp.Services.Diagnostics; using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Services; diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs index c7dc00d9a0..7b6229b620 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs @@ -13,7 +13,6 @@ using OmniSharp.Models; using OmniSharp.Roslyn.CSharp.Services.CodeActions; using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; -using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using RunCodeActionRequest = OmniSharp.Models.V2.CodeActions.RunCodeActionRequest; using RunCodeActionResponse = OmniSharp.Models.V2.CodeActions.RunCodeActionResponse; @@ -96,123 +95,5 @@ public override async Task Handle(RunCodeActionRequest re Changes = changes }; } - - private async Task<(Solution Solution, IEnumerable FileChanges)> GetFileChangesAsync(Solution newSolution, Solution oldSolution, string directory, bool wantTextChanges, bool wantsAllCodeActionOperations) - { - var solution = oldSolution; - var filePathToResponseMap = new Dictionary(); - var solutionChanges = newSolution.GetChanges(oldSolution); - - foreach (var projectChange in solutionChanges.GetProjectChanges()) - { - // Handle added documents - foreach (var documentId in projectChange.GetAddedDocuments()) - { - var newDocument = newSolution.GetDocument(documentId); - var text = await newDocument.GetTextAsync(); - - var newFilePath = newDocument.FilePath == null || !Path.IsPathRooted(newDocument.FilePath) - ? Path.Combine(directory, newDocument.Name) - : newDocument.FilePath; - - var modifiedFileResponse = new ModifiedFileResponse(newFilePath) - { - Changes = new[] { - new LinePositionSpanTextChange - { - NewText = text.ToString() - } - } - }; - - filePathToResponseMap[newFilePath] = modifiedFileResponse; - - // We must add new files to the workspace to ensure that they're present when the host editor - // tries to modify them. This is a strange interaction because the workspace could be left - // in an incomplete state if the host editor doesn't apply changes to the new file, but it's - // what we've got today. - if (this.Workspace.GetDocument(newFilePath) == null) - { - var fileInfo = new FileInfo(newFilePath); - if (!fileInfo.Exists) - { - fileInfo.CreateText().Dispose(); - } - else - { - // The file already exists on disk? Ensure that it's zero-length. If so, we can still use it. - if (fileInfo.Length > 0) - { - Logger.LogError($"File already exists on disk: '{newFilePath}'"); - break; - } - } - - this.Workspace.AddDocument(documentId, projectChange.NewProject, newFilePath, newDocument.SourceCodeKind); - solution = this.Workspace.CurrentSolution; - } - else - { - // The file already exists in the workspace? We're in a bad state. - Logger.LogError($"File already exists in workspace: '{newFilePath}'"); - } - } - - // Handle changed documents - foreach (var documentId in projectChange.GetChangedDocuments()) - { - var newDocument = newSolution.GetDocument(documentId); - var oldDocument = oldSolution.GetDocument(documentId); - var filePath = newDocument.FilePath; - - // file rename - if (oldDocument != null && newDocument.Name != oldDocument.Name) - { - if (wantsAllCodeActionOperations) - { - var newFilePath = GetNewFilePath(newDocument.Name, oldDocument.FilePath); - var text = await oldDocument.GetTextAsync(); - var temp = solution.RemoveDocument(documentId); - solution = temp.AddDocument(DocumentId.CreateNewId(oldDocument.Project.Id, newDocument.Name), newDocument.Name, text, oldDocument.Folders, newFilePath); - - filePathToResponseMap[filePath] = new RenamedFileResponse(oldDocument.FilePath, newFilePath); - filePathToResponseMap[newFilePath] = new OpenFileResponse(newFilePath); - } - continue; - } - - if (!filePathToResponseMap.TryGetValue(filePath, out var fileOperationResponse)) - { - fileOperationResponse = new ModifiedFileResponse(filePath); - filePathToResponseMap[filePath] = fileOperationResponse; - } - - if (fileOperationResponse is ModifiedFileResponse modifiedFileResponse) - { - if (wantTextChanges) - { - var linePositionSpanTextChanges = await TextChanges.GetAsync(newDocument, oldDocument); - - modifiedFileResponse.Changes = modifiedFileResponse.Changes != null - ? modifiedFileResponse.Changes.Union(linePositionSpanTextChanges) - : linePositionSpanTextChanges; - } - else - { - var text = await newDocument.GetTextAsync(); - modifiedFileResponse.Buffer = text.ToString(); - } - } - } - } - - return (solution, filePathToResponseMap.Values); - } - - private static string GetNewFilePath(string newFileName, string currentFilePath) - { - var directory = Path.GetDirectoryName(currentFilePath); - return Path.Combine(directory, newFileName); - } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs index 64699d996f..06f7816e73 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs @@ -1,27 +1,18 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Composition; using System.Linq; using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Reflection; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Logging; using OmniSharp.Helpers; using OmniSharp.Models.Diagnostics; -using OmniSharp.Options; -using OmniSharp.Roslyn; -using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; -using OmniSharp.Services; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics { @@ -143,11 +134,11 @@ public ImmutableArray QueueForDiagnosis(ImmutableArray docum return ImmutableArray.Empty; } - public async Task> GetDiagnostics(ImmutableArray documentPaths) + public async Task> GetDiagnostics(ImmutableArray documentPaths) { - if (!documentPaths.Any()) return ImmutableArray<(string projectName, Diagnostic diagnostic)>.Empty; + if (!documentPaths.Any()) return ImmutableArray.Empty; - var results = new List<(string projectName, Diagnostic diagnostic)>(); + var results = new List(); var documents = (await Task.WhenAll( @@ -162,7 +153,7 @@ public ImmutableArray QueueForDiagnosis(ImmutableArray docum var projectName = document.Project.Name; var diagnostics = await GetDiagnosticsForDocument(document, projectName); - results.AddRange(diagnostics.Select(x => (projectName: document.Project.Name, diagnostic: x))); + results.Add(new DocumentDiagnostics(document.Id, document.FilePath, document.Project.Id, document.Project.Name, diagnostics)); } return results.ToImmutableArray(); @@ -198,7 +189,7 @@ public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray x.Id).ToImmutableArray(); } - public Task> GetAllDiagnosticsAsync() + public Task> GetAllDiagnosticsAsync() { var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).Select(x => x.FilePath).ToImmutableArray(); return GetDiagnostics(documents); diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs index 09e4055f02..a7bf6c94b6 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -25,9 +25,8 @@ public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker private readonly AnalyzerWorkQueue _workQueue; private readonly ILogger _logger; - private readonly ConcurrentDictionary diagnostics)> _currentDiagnosticResults = - new ConcurrentDictionary diagnostics)>(); - + private readonly ConcurrentDictionary _currentDiagnosticResultLookup = + new ConcurrentDictionary(); private readonly ImmutableArray _providers; private readonly DiagnosticEventForwarder _forwarder; private readonly OmniSharpOptions _options; @@ -37,7 +36,6 @@ public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker // Currently roslyn doesn't expose official way to use IDE analyzers during analysis. // This options gives certain IDE analysis access for services that are not yet publicly available. private readonly ConstructorInfo _workspaceAnalyzerOptionsConstructor; - private bool _initialSolutionAnalysisInvoked = false; public CSharpDiagnosticWorkerWithAnalyzers( OmniSharpWorkspace workspace, @@ -64,47 +62,43 @@ public CSharpDiagnosticWorkerWithAnalyzers( Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Foreground), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Background), TaskCreationOptions.LongRunning); - } - private Task InitializeWithWorkspaceDocumentsIfNotYetDone() - { - if (_initialSolutionAnalysisInvoked) - return Task.CompletedTask; + _workspace.OnInitialized += (isInitialized) => OnWorkspaceInitialized(isInitialized); - _initialSolutionAnalysisInvoked = true; + OnWorkspaceInitialized(_workspace.Initialized); + } - return Task.Run(async () => - { - while (!_workspace.Initialized || _workspace.CurrentSolution.Projects.Count() == 0) await Task.Delay(50); - }) - .ContinueWith(_ => Task.Delay(50)) - .ContinueWith(_ => + public void OnWorkspaceInitialized(bool isInitialized) + { + if (isInitialized) { var documentIds = QueueDocumentsForDiagnostics(); _logger.LogInformation($"Solution initialized -> queue all documents for code analysis. Initial document count: {documentIds.Length}."); - }); + } } - public async Task> GetDiagnostics(ImmutableArray documentPaths) + public async Task> GetDiagnostics(ImmutableArray documentPaths) { - await InitializeWithWorkspaceDocumentsIfNotYetDone(); - var documentIds = GetDocumentIdsFromPaths(documentPaths); - return await GetDiagnosticsByDocumentIds(documentIds); + return await GetDiagnosticsByDocumentIds(documentIds, waitForDocuments: true); } - private async Task> GetDiagnosticsByDocumentIds(ImmutableArray documentIds) + private async Task> GetDiagnosticsByDocumentIds(ImmutableArray documentIds, bool waitForDocuments) { - if (documentIds.Length == 1) + if (waitForDocuments) { - _workQueue.TryPromote(documentIds.Single()); + foreach (var documentId in documentIds) + { + _workQueue.TryPromote(documentId); + } + await _workQueue.WaitForegroundWorkComplete(); } - return _currentDiagnosticResults - .Where(x => documentIds.Any(docId => docId == x.Key)) - .SelectMany(x => x.Value.diagnostics, (k, v) => ((k.Value.projectName, v))) + return documentIds + .Where(x => _currentDiagnosticResultLookup.ContainsKey(x)) + .Select(x => _currentDiagnosticResultLookup[x]) .ToImmutableArray(); } @@ -112,6 +106,7 @@ private ImmutableArray GetDocumentIdsFromPaths(ImmutableArray _workspace.GetDocumentId(docPath)) + .Where(x => x != default) .ToImmutableArray(); } @@ -174,7 +169,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEv QueueForAnalysis(ImmutableArray.Create(changeEvent.DocumentId), AnalyzerWorkType.Foreground); break; case WorkspaceChangeKind.DocumentRemoved: - if (!_currentDiagnosticResults.TryRemove(changeEvent.DocumentId, out _)) + if (!_currentDiagnosticResultLookup.TryRemove(changeEvent.DocumentId, out _)) { _logger.LogDebug($"Tried to remove non existent document from analysis, document: {changeEvent.DocumentId}"); } @@ -186,6 +181,12 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEv var projectDocumentIds = _workspace.CurrentSolution.GetProject(changeEvent.ProjectId).Documents.Select(x => x.Id).ToImmutableArray(); QueueForAnalysis(projectDocumentIds, AnalyzerWorkType.Background); break; + case WorkspaceChangeKind.SolutionAdded: + case WorkspaceChangeKind.SolutionChanged: + case WorkspaceChangeKind.SolutionReloaded: + QueueDocumentsForDiagnostics(); + break; + } } @@ -279,11 +280,11 @@ private void OnAnalyzerException(Exception ex, DiagnosticAnalyzer analyzer, Diag private void UpdateCurrentDiagnostics(Project project, Document document, ImmutableArray diagnosticsWithAnalyzers) { - _currentDiagnosticResults[document.Id] = (project.Name, diagnosticsWithAnalyzers); - EmitDiagnostics(document.FilePath, _currentDiagnosticResults[document.Id].diagnostics); + _currentDiagnosticResultLookup[document.Id] = new DocumentDiagnostics(document.Id, document.FilePath, project.Id, project.Name, diagnosticsWithAnalyzers); + EmitDiagnostics(_currentDiagnosticResultLookup[document.Id]); } - private void EmitDiagnostics(string filePath, ImmutableArray results) + private void EmitDiagnostics(DocumentDiagnostics results) { _forwarder.Forward(new DiagnosticMessage { @@ -291,9 +292,8 @@ private void EmitDiagnostics(string filePath, ImmutableArray results { new DiagnosticResult { - FileName = filePath, QuickFixes = results + FileName = results.DocumentPath, QuickFixes = results.Diagnostics .Select(x => x.ToDiagnosticLocation()) - .Where(x => x.FileName == filePath) .ToList() } } @@ -307,11 +307,10 @@ public ImmutableArray QueueDocumentsForDiagnostics() return documentIds; } - public async Task> GetAllDiagnosticsAsync() + public async Task> GetAllDiagnosticsAsync() { - await InitializeWithWorkspaceDocumentsIfNotYetDone(); var allDocumentsIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray(); - return await GetDiagnosticsByDocumentIds(allDocumentsIds); + return await GetDiagnosticsByDocumentIds(allDocumentsIds, waitForDocuments: false); } public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectIds) diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs index 8458c09748..c970cb9b8c 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs @@ -1,21 +1,11 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Linq; -using System.Reflection; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Logging; -using OmniSharp.Helpers; -using OmniSharp.Models.Diagnostics; using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.Diagnostics; -using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics @@ -26,7 +16,6 @@ namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics public class CsharpDiagnosticWorkerComposer: ICsDiagnosticWorker { private readonly ICsDiagnosticWorker _implementation; - private readonly OmniSharpWorkspace _workspace; [ImportingConstructor] public CsharpDiagnosticWorkerComposer( @@ -44,16 +33,14 @@ public CsharpDiagnosticWorkerComposer( { _implementation = new CSharpDiagnosticWorker(workspace, forwarder, loggerFactory); } - - _workspace = workspace; } - public Task> GetAllDiagnosticsAsync() + public Task> GetAllDiagnosticsAsync() { return _implementation.GetAllDiagnosticsAsync(); } - public Task> GetDiagnostics(ImmutableArray documentPaths) + public Task> GetDiagnostics(ImmutableArray documentPaths) { return _implementation.GetDiagnostics(documentPaths); } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/DocumentDiagnostics.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/DocumentDiagnostics.cs new file mode 100644 index 0000000000..d4b7d2c6c2 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/DocumentDiagnostics.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics +{ + public class DocumentDiagnostics + { + public DocumentDiagnostics(DocumentId documentId, string documentPath, ProjectId projectId, string projectName, ImmutableArray diagnostics) + { + DocumentId = documentId; + DocumentPath = documentPath; + ProjectId = projectId; + ProjectName = projectName; + Diagnostics = diagnostics; + } + + public DocumentId DocumentId { get; } + public ProjectId ProjectId { get; } + public string ProjectName { get; } + public string DocumentPath { get; } + public ImmutableArray Diagnostics { get; } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs index 1d22d70304..2297d04b3f 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs @@ -1,13 +1,14 @@ using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics { public interface ICsDiagnosticWorker { - Task> GetDiagnostics(ImmutableArray documentPaths); - Task> GetAllDiagnosticsAsync(); + Task> GetDiagnostics(ImmutableArray documentPaths); + Task> GetAllDiagnosticsAsync(); ImmutableArray QueueDocumentsForDiagnostics(); ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectId); } diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index 05b7faf8ed..4ae4385e53 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -25,7 +25,19 @@ namespace OmniSharp [Export, Shared] public class OmniSharpWorkspace : Workspace { - public bool Initialized { get; set; } + public bool Initialized + { + get { return isInitialized; } + set + { + if (isInitialized == value) return; + isInitialized = value; + OnInitialized(isInitialized); + } + } + + public event Action OnInitialized = delegate { }; + public bool EditorConfigEnabled { get; set; } public BufferManager BufferManager { get; private set; } @@ -34,6 +46,7 @@ public class OmniSharpWorkspace : Workspace private readonly ConcurrentBag> _waitForProjectModelReadyHandlers = new ConcurrentBag>(); private readonly ConcurrentDictionary miscDocumentsProjectInfos = new ConcurrentDictionary(); private readonly ConcurrentDictionary> documentInclusionRulesPerProject = new ConcurrentDictionary>(); + private bool isInitialized; [ImportingConstructor] public OmniSharpWorkspace(HostServicesAggregator aggregator, ILoggerFactory loggerFactory, IFileSystemWatcher fileSystemWatcher) diff --git a/tests/OmniSharp.Cake.Tests/CodeActionsV2Facts.cs b/tests/OmniSharp.Cake.Tests/CodeActionsV2Facts.cs index de04f08787..c665bbd346 100644 --- a/tests/OmniSharp.Cake.Tests/CodeActionsV2Facts.cs +++ b/tests/OmniSharp.Cake.Tests/CodeActionsV2Facts.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -120,9 +121,9 @@ public void Whatever() private async Task RunRefactoringAsync(string code, string refactoringName) { var refactorings = await FindRefactoringsAsync(code); - Assert.Contains(refactoringName, refactorings.Select(a => a.Name)); + Assert.Contains(refactoringName, refactorings.Select(x => x.Name), StringComparer.OrdinalIgnoreCase); - var identifier = refactorings.First(action => action.Name.Equals(refactoringName)).Identifier; + var identifier = refactorings.First(action => action.Name.Equals(refactoringName, StringComparison.OrdinalIgnoreCase)).Identifier; return await RunRefactoringsAsync(code, identifier); } diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs index 8a21d662b9..309405bb8d 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.CodeAnalysis; diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs index 60b6498c27..75bcde6e72 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs @@ -87,7 +87,7 @@ public class c {public c() {Guid.NewGuid();}}"; public class c {public c() {Guid.NewGuid();}}"; var response = await RunRefactoringAsync(code, "Remove Unnecessary Usings", isAnalyzersEnabled: roslynAnalyzersEnabled); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Theory] @@ -185,8 +185,8 @@ private static void NewMethod() Console.Write(""should be using System;""); } }"; - var response = await RunRefactoringAsync(code, "Extract method", isAnalyzersEnabled: roslynAnalyzersEnabled); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + var response = await RunRefactoringAsync(code, "Extract Method", isAnalyzersEnabled: roslynAnalyzersEnabled); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Theory] diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs index 6a6e687a91..86b1de7a5a 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using OmniSharp.Models; -using OmniSharp.Models.V2; -using OmniSharp.Models.V2.CodeActions; -using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; using TestUtility; using Xunit; using Xunit.Abstractions; @@ -38,10 +32,9 @@ public Class1(string propertyHere) } public string PropertyHere { get; set; } - } - "; + }"; var response = await RunRefactoringAsync(code, "Generate constructor..."); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Fact] @@ -51,6 +44,7 @@ public async Task Can_generate_overrides() @"public class Class1[||] { }"; + const string expected = @"public class Class1 { @@ -68,10 +62,10 @@ public override string ToString() { return base.ToString(); } - } - "; + }"; + var response = await RunRefactoringAsync(code, "Generate overrides..."); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Fact] @@ -82,6 +76,7 @@ public async Task Can_generate_equals_for_object() { public string PropertyHere { get; set; } }"; + const string expected = @"public class Class1 { @@ -92,10 +87,10 @@ public override bool Equals(object obj) return obj is Class1 @class && PropertyHere == @class.PropertyHere; } - } - "; + }"; + var response = await RunRefactoringAsync(code, "Generate Equals(object)..."); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Fact] @@ -156,8 +151,7 @@ public class Class1: BaseClass public string Foo[||] { get; set; } } - public class BaseClass {} -"; + public class BaseClass {}"; var response = await FindRefactoringNamesAsync(code); @@ -174,8 +168,7 @@ public async Task Can_extract_interface() }"; const string expected = - @" - public interface IClass1 + @"public interface IClass1 { string PropertyHere { get; set; } } @@ -183,11 +176,11 @@ public interface IClass1 public class Class1 : IClass1 { public string PropertyHere { get; set; } - } - "; + }"; + var response = await RunRefactoringAsync(code, "Extract interface..."); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } [Fact] diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs index d588ff8567..a4b53a54e7 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs @@ -129,7 +129,7 @@ public async Task Always_return_results_from_net_default_analyzers() AddProjectWitFile(host, testFile); - var result = await host.RequestCodeCheckAsync(); + var result = await host.RequestCodeCheckAsync(testFile.FileName); Assert.Contains(result.QuickFixes.OfType().Where(x => x.FileName == testFile.FileName), f => f.Id.Contains("CS")); } @@ -155,7 +155,7 @@ static void Main(string[] args) host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRules.ToImmutableDictionary()); - var result = await host.RequestCodeCheckAsync(); + var result = await host.RequestCodeCheckAsync(testFile.FileName); Assert.Contains(result.QuickFixes.OfType(), f => f.Id == "CS0162" && f.LogLevel == "Hidden"); } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/EditorConfigFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/EditorConfigFacts.cs index ac0722888c..2d9d0d632e 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/EditorConfigFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/EditorConfigFacts.cs @@ -218,7 +218,7 @@ public Foo() ["RoslynExtensionsOptions:EnableAnalyzersSupport"] = "true" }, TestAssets.Instance.TestFilesFolder)) { - var result = await host.RequestCodeCheckAsync(); + var result = await host.RequestCodeCheckAsync(testFile.FileName); Assert.Contains(result.QuickFixes.OfType().Where(x => x.FileName == testFile.FileName), f => f.Text == "Use framework type" && f.Id == "IDE0049"); Assert.Contains(result.QuickFixes.OfType().Where(x => x.FileName == testFile.FileName), f => f.Text == "Use explicit type instead of 'var'" && f.Id == "IDE0008"); @@ -247,7 +247,7 @@ public Foo(string bar) ["RoslynExtensionsOptions:EnableAnalyzersSupport"] = "true" }, TestAssets.Instance.TestFilesFolder)) { - var result = await host.RequestCodeCheckAsync(); + var result = await host.RequestCodeCheckAsync(testFile.FileName); Assert.Contains(result.QuickFixes.OfType().Where(x => x.FileName == testFile.FileName), f => f.Text == "Naming rule violation: Missing prefix: 'xxx_'" && f.Id == "IDE1006"); } @@ -331,7 +331,7 @@ public Foo(string something) }; var runResponse = await runRequestHandler.Handle(runRequest); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); } } @@ -381,18 +381,8 @@ public Foo() }; var runResponse = await runRequestHandler.Handle(runRequest); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); } } - - private static void AssertIgnoringIndent(string expected, string actual) - { - Assert.Equal(TrimLines(expected), TrimLines(actual), false, true, true); - } - - private static string TrimLines(string source) - { - return string.Join("\n", source.Split('\n').Select(s => s.Trim())); - } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/FixAllFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/FixAllFacts.cs new file mode 100644 index 0000000000..2d7bb81940 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/FixAllFacts.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Abstractions.Models.V1.FixAll; +using OmniSharp.Models; +using OmniSharp.Models.Events; +using OmniSharp.Roslyn.CSharp.Services.Refactoring; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class FixAllFacts + { + private readonly ITestOutputHelper _testOutput; + private readonly TestEventEmitter _analysisEventListener; + + public FixAllFacts(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _analysisEventListener = new TestEventEmitter(); + } + + [Fact] + public async Task WhenFileContainsFixableIssuesWithAnalyzersEnabled_ThenFixThemAll() + { + using (var host = GetHost(true)) + { + var originalText = @"class C {}"; + + var expectedText = @"internal class C { }"; + + var testFilePath = CreateTestProjectWithDocument(host, originalText); + + string textBeforeFix = await GetContentOfDocumentFromWorkspace(host, testFilePath); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + var response = await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Document, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }); + + string textAfterFix = await GetContentOfDocumentFromWorkspace(host, testFilePath); + AssertUtils.AssertIgnoringIndent(textAfterFix, expectedText); + + var internalClassChange = response.Changes.OfType().Single().Changes.Single(x => x.NewText == "internal "); + + Assert.Equal(0, internalClassChange.StartLine); + Assert.Equal(0, internalClassChange.StartColumn); + Assert.Equal(0, internalClassChange.EndLine); + Assert.Equal(0, internalClassChange.EndColumn); + + var formatFix = response.Changes.OfType().Single().Changes.Single(x => x.NewText == " "); + + Assert.Equal(0, formatFix.StartLine); + Assert.Equal(9, formatFix.StartColumn); + Assert.Equal(0, formatFix.EndLine); + Assert.Equal(9, formatFix.EndColumn); + } + } + + [Fact] + public async Task WhenFixAllItemsAreDefinedByFilter_ThenFixOnlyFilteredItems() + { + using (var host = GetHost(true)) + { + var originalText = + @" + class C{} + "; + + // If filtering isn't set, this should also add 'internal' etc which + // should not appear now as result. + var expectedText = + @" + class C { } + "; + + var testFilePath = CreateTestProjectWithDocument(host, originalText); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Document, + FileName = testFilePath, + FixAllFilter = new[] { new FixAllItem("IDE0055", "Fix formatting") }, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }); + + string textAfterFix = await GetContentOfDocumentFromWorkspace(host, testFilePath); + + AssertUtils.AssertIgnoringIndent(expectedText, textAfterFix); + } + } + + [Theory] + [InlineData(FixAllScope.Document)] + [InlineData(FixAllScope.Project)] + public async Task WhenFixAllIsScopedToDocumentAndProject_ThenOnlyFixInScopeInsteadOfEverything(FixAllScope scope) + { + using (var host = GetHost(true)) + { + var originalIde0055Text = + @" + internal class InvalidFormatIDE0055ExpectedHere{} + "; + + var expectedIde0055TextWithFixedFormat = + @" + internal class InvalidFormatIDE0055ExpectedHere { } + "; + + var fileInScope = CreateTestProjectWithDocument(host, originalIde0055Text); + + var fileNotInScope = CreateTestProjectWithDocument(host, originalIde0055Text); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + await handler.Handle(new RunFixAllRequest + { + Scope = scope, + FileName = fileInScope, + FixAllFilter = new[] { new FixAllItem("IDE0055", "Fix formatting") }, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }); + + string textAfterFixInScope = await GetContentOfDocumentFromWorkspace(host, fileInScope); + string textAfterNotInScope = await GetContentOfDocumentFromWorkspace(host, fileNotInScope); + + AssertUtils.AssertIgnoringIndent(expectedIde0055TextWithFixedFormat, textAfterFixInScope); + AssertUtils.AssertIgnoringIndent(originalIde0055Text, textAfterNotInScope); + } + } + + [Fact(Skip = @"Fails on windows only inside roslyn +System.ArgumentOutOfRangeException +Specified argument was out of the range of valid values. +Parameter name: start +... +")] + // This is specifically tested because has custom mapping logic in it. + public async Task WhenTextContainsUnusedImports_ThenTheyCanBeAutomaticallyFixed() + { + using (var host = GetHost(true)) + { + var originalText = + @" + using System.IO; + "; + + var expectedText = @""; + + var testFilePath = CreateTestProjectWithDocument(host, originalText); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Document, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }); + + string textAfterFix = await GetContentOfDocumentFromWorkspace(host, testFilePath); + + AssertUtils.AssertIgnoringIndent(expectedText, textAfterFix); + } + } + + [Fact()] + public async Task WhenIssueThatCannotBeAutomaticallyFixedIsAvailable_ThenDontTryToFixIt() + { + using (var host = GetHost(true)) + { + var originalText = + @" + invalidSyntaxThatCannotBeFixedHere + "; + + var expectedText = + @" + invalidSyntaxThatCannotBeFixedHere + "; + + var testFilePath = CreateTestProjectWithDocument(host, originalText); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Document, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }); + + string textAfterFix = await GetContentOfDocumentFromWorkspace(host, testFilePath); + + AssertUtils.AssertIgnoringIndent(textAfterFix, expectedText); + } + } + + [Fact] + public async Task WhenAvailableFixAllActionsAreRequested_ThenReturnThemAtResponse() + { + using (var host = GetHost(true)) + { + var ide0055File = CreateTestProjectWithDocument(host, + @" + internal class InvalidFormatIDE0055ExpectedHere{} + "); + + var resultFromDocument = await GetFixAllTargets(host, ide0055File, FixAllScope.Document); + + Assert.Contains(resultFromDocument.Items, x => x.Id == "IDE0055"); + Assert.DoesNotContain(resultFromDocument.Items, x => x.Id == "IDE0040"); + } + } + + [Theory] + [InlineData(FixAllScope.Document)] + [InlineData(FixAllScope.Project)] + public async Task WhenGetActionIsScoped_ThenReturnOnlyItemsFromCorrectScope(FixAllScope scope) + { + using (var host = GetHost(true)) + { + var inScopeFile = CreateTestProjectWithDocument(host, + @" + internal class InvalidFormatIDE0055ExpectedHere{} + "); + + var notInScopeFile = CreateTestProjectWithDocument(host, + @" + class NonInternalIDEIDE0040 { } + "); + + var resultFromDocument = await GetFixAllTargets(host, inScopeFile, scope); + + Assert.Contains(resultFromDocument.Items, x => x.Id == "IDE0055"); + Assert.DoesNotContain(resultFromDocument.Items, x => x.Id == "IDE0040"); + } + } + + [Fact] + // Currently fix every problem in project or solution scope is not supported, only documents. + public async Task WhenProjectOrSolutionIsScopedWithFixEverything_ThenThrowNotImplementedException() + { + using (var host = GetHost(true)) + { + var textWithFormattingProblem = + @" + class C{} + "; + + var testFilePath = CreateTestProjectWithDocument(host, textWithFormattingProblem); + + var handler = host.GetRequestHandler(OmniSharpEndpoints.RunFixAll); + + await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Document, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true, + FixAllFilter = null // This means: try fix everything. + }); + + await Assert.ThrowsAsync(async () => await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Project, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true, + FixAllFilter = null // This means: try fix everything. + })); + + await Assert.ThrowsAsync(async () => await handler.Handle(new RunFixAllRequest + { + Scope = FixAllScope.Solution, + FileName = testFilePath, + WantsTextChanges = true, + WantsAllCodeActionOperations = true, + FixAllFilter = null + })); + } + } + + private OmniSharpTestHost GetHost(bool roslynAnalyzersEnabled) + { + return OmniSharpTestHost.Create( + testOutput: _testOutput, + configurationData: new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }.ToConfiguration(), + eventEmitter: _analysisEventListener + ); + } + + private static string CreateTestProjectWithDocument(OmniSharpTestHost host, string content) + { + var fileName = $"{Guid.NewGuid()}.cs"; + var projectId = host.AddFilesToWorkspace(new TestFile(fileName, content)).First(); + return host.Workspace.CurrentSolution.GetProject(projectId).Documents.Single().FilePath; + } + + private static async Task GetFixAllTargets(OmniSharpTestHost host, string fileName, FixAllScope scope) + { + var handler = host.GetRequestHandler(OmniSharpEndpoints.GetFixAll); + + return await handler.Handle(new GetFixAllRequest() + { + FileName = fileName, + Scope = scope + }); + } + + private static async Task GetContentOfDocumentFromWorkspace(OmniSharpTestHost host, string testFilePath) + { + var docAfterUpdate = host.Workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).First(x => x.FilePath == testFilePath); + return (await docAfterUpdate.GetTextAsync()).ToString(); + } + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/ImplementTypeFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/ImplementTypeFacts.cs index 9bd7519e53..f4a245c226 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/ImplementTypeFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/ImplementTypeFacts.cs @@ -130,7 +130,7 @@ private async Task VerifyImplementType(string code, string expectedChange, Dicti Assert.Single(changes); Assert.NotNull(changes[0].FileName); - AssertIgnoringIndent(expectedChange, ((ModifiedFileResponse)changes[0]).Changes.First().NewText); + AssertUtils.AssertIgnoringIndentAndNewlines(expectedChange, ((ModifiedFileResponse)changes[0]).Changes.First().NewText); } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/SyncNamespaceFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/SyncNamespaceFacts.cs index 43460cf9b8..9a2cc7dbaf 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/SyncNamespaceFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/SyncNamespaceFacts.cs @@ -98,7 +98,7 @@ public async Task RespectFolderName_InExecutedCodeActions(string expectedNamespa }; var runResponse = await runRequestHandler.Handle(runRequest); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); } } @@ -131,7 +131,7 @@ public async Task RespectFolderName_InExecutedCodeActions_AfterLiveChange(string }; var runResponse = await runRequestHandler.Handle(runRequest); - AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); + AssertUtils.AssertIgnoringIndent(expected, ((ModifiedFileResponse)runResponse.Changes.First()).Buffer); } } @@ -151,15 +151,5 @@ public void CheckIfOnDefaultNamespaceChangedIsAvailableInRoslyn() Assert.Equal(typeof(ProjectId), parameters[0].ParameterType); Assert.Equal(typeof(string), parameters[1].ParameterType); } - - private static void AssertIgnoringIndent(string expected, string actual) - { - Assert.Equal(TrimLines(expected), TrimLines(actual), false, true, true); - } - - private static string TrimLines(string source) - { - return string.Join("\n", source.Split('\n').Select(s => s.Trim())); - } } } diff --git a/tests/TestUtility/AbstractCodeActionsTestFixture.cs b/tests/TestUtility/AbstractCodeActionsTestFixture.cs index 568480a027..6237e0b20b 100644 --- a/tests/TestUtility/AbstractCodeActionsTestFixture.cs +++ b/tests/TestUtility/AbstractCodeActionsTestFixture.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Composition.Hosting.Core; using System.Globalization; using System.IO; using System.Linq; @@ -55,9 +57,9 @@ protected OmniSharpTestHost CreateOmniSharpHost(TestFile[] testFiles, IConfigura protected async Task RunRefactoringAsync(string code, string refactoringName, bool wantsChanges = false, bool isAnalyzersEnabled = true) { var refactorings = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); - Assert.Contains(refactoringName, refactorings.Select(a => a.Name)); + Assert.Contains(refactoringName, refactorings.Select(x => x.Name), StringComparer.OrdinalIgnoreCase); - var identifier = refactorings.First(action => action.Name.Equals(refactoringName)).Identifier; + var identifier = refactorings.First(action => action.Name.Equals(refactoringName, StringComparison.OrdinalIgnoreCase)).Identifier; return await RunRefactoringsAsync(code, identifier, wantsChanges); } diff --git a/tests/TestUtility/AssertUtils.cs b/tests/TestUtility/AssertUtils.cs new file mode 100644 index 0000000000..f67f32d2ad --- /dev/null +++ b/tests/TestUtility/AssertUtils.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Xunit; + +namespace TestUtility +{ + public static class AssertUtils + { + public static void AssertIgnoringIndent(string expected, string actual) + { + Assert.Equal(TrimLines(expected), TrimLines(actual), false, true, true); + } + + public static void AssertIgnoringIndentAndNewlines(string expected, string actual) + { + Assert.Equal(TrimAndRemoveNewLines(expected), TrimAndRemoveNewLines(actual), false, true, true); + } + + private static string TrimAndRemoveNewLines(string source) + { + return string.Join("", source.Split('\n').Select(s => s.Trim())); + } + + private static string TrimLines(string source) + { + return string.Join("\n", source.Split('\n').Select(s => s.Trim())); + } + } +} \ No newline at end of file