diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddInstanceMemberParameter.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddInstanceMemberParameter.fs index e3180651f9b..6121af4da44 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddInstanceMemberParameter.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddInstanceMemberParameter.fs @@ -13,27 +13,13 @@ type internal FSharpAddInstanceMemberParameterCodeFixProvider() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0673" ] + static let title = SR.AddMissingInstanceMemberParameter() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context : Task = asyncMaybe { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let title = SR.AddMissingInstanceMemberParameter() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.AddInstanceMemberParameter, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(context.Span.Start, 0), "x.") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.AddInstanceMemberParameter, title, [| TextChange(TextSpan(context.Span.Start, 0), "x.") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddMissingEqualsToTypeDefinition.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddMissingEqualsToTypeDefinition.fs index f667c624e3b..673f1bffee5 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddMissingEqualsToTypeDefinition.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddMissingEqualsToTypeDefinition.fs @@ -14,15 +14,11 @@ type internal FSharpAddMissingEqualsToTypeDefinitionCodeFixProvider() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS3360" ] - + static let title = SR.AddMissingEqualsToTypeDefinition() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context : Task = asyncMaybe { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray let! sourceText = context.Document.GetTextAsync(context.CancellationToken) @@ -37,19 +33,14 @@ type internal FSharpAddMissingEqualsToTypeDefinitionCodeFixProvider() = pos <- pos - 1 ch <- sourceText.[pos] - let title = SR.AddMissingEqualsToTypeDefinition() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( + do + context.RegisterFsharpFix( CodeFix.AddMissingEqualsToTypeDefinition, title, - context, // 'pos + 1' is here because 'pos' is now the position of the first non-whitespace character. // Using just 'pos' will creat uncompilable code. - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(pos + 1, 0), " =") |]) + [| TextChange(TextSpan(pos + 1, 0), " =") |] ) - - context.RegisterCodeFix(codeFix, diagnostics) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddMissingFunKeyword.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddMissingFunKeyword.fs index 234c4bd0594..597190a7d17 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddMissingFunKeyword.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddMissingFunKeyword.fs @@ -14,6 +14,7 @@ open FSharp.Compiler.CodeAnalysis [] type internal FSharpAddMissingFunKeywordCodeFixProvider [] () = inherit CodeFixProvider() + static let title = SR.AddMissingFunKeyword() let fixableDiagnosticIds = set [ "FS0010" ] @@ -56,22 +57,7 @@ type internal FSharpAddMissingFunKeywordCodeFixProvider [] let! intendedArgSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range) - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let title = SR.AddMissingFunKeyword() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.AddMissingFunKeyword, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.AddMissingFunKeyword, title, [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddNewKeywordToDisposableConstructorInvocation.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddNewKeywordToDisposableConstructorInvocation.fs index d6dcfd30d34..8a72534a804 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddNewKeywordToDisposableConstructorInvocation.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddNewKeywordToDisposableConstructorInvocation.fs @@ -3,36 +3,37 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.CodeActions [] type internal FSharpAddNewKeywordCodeFixProvider() = inherit CodeFixProvider() - static let fixableDiagnosticIds = set [ "FS0760" ] + static let title = SR.AddNewKeyword() + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0760" - override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds + member this.GetChanges(_document: Document, diagnostics: ImmutableArray, _ct: CancellationToken) = + backgroundTask { - override _.RegisterCodeFixesAsync context : Task = - async { - let title = SR.AddNewKeyword() + let changes = + diagnostics + |> Seq.map (fun d -> TextChange(TextSpan(d.Location.SourceSpan.Start, 0), "new ")) - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.AddNewKeyword, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(context.Span.Start, 0), "new ") |]) - ) + return changes + } - context.RegisterCodeFix(codeFix, diagnostics) + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.AddNewKeyword, title, changes) } - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.AddNewKeyword this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs index dcb40c4cb6c..0b30fe67920 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/AddOpenCodeFixProvider.fs @@ -30,7 +30,7 @@ type internal FSharpAddOpenCodeFixProvider [] (assemblyCon CodeFix.AddOpen, fixUnderscoresInMenuText fullName, context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, qualifier) |]) + [| TextChange(context.Span, qualifier) |] ) let openNamespaceFix (context: CodeFixContext) ctx name ns multipleNames = @@ -77,12 +77,7 @@ type internal FSharpAddOpenCodeFixProvider [] (assemblyCon |> Seq.toList for codeFix in openNamespaceFixes @ qualifiedSymbolFixes do - context.RegisterCodeFix( - codeFix, - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id) - |> Seq.toImmutableArray - ) + context.RegisterCodeFix(codeFix, context.Diagnostics) override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ChangePrefixNegationToInfixSubtraction.fs b/vsintegration/src/FSharp.Editor/CodeFix/ChangePrefixNegationToInfixSubtraction.fs index 12296f0bd67..8d82926e692 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ChangePrefixNegationToInfixSubtraction.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ChangePrefixNegationToInfixSubtraction.fs @@ -14,16 +14,12 @@ type internal FSharpChangePrefixNegationToInfixSubtractionodeFixProvider() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0003" ] + static let title = SR.ChangePrefixNegationToInfixSubtraction() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context : Task = asyncMaybe { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) let mutable pos = context.Span.End + 1 @@ -39,18 +35,7 @@ type internal FSharpChangePrefixNegationToInfixSubtractionodeFixProvider() = // Bail if this isn't a negation do! Option.guard (ch = '-') - - let title = SR.ChangePrefixNegationToInfixSubtraction() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ChangePrefixNegationToInfixSubtraction, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(pos, 1), "- ") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ChangePrefixNegationToInfixSubtraction, title, [| TextChange(TextSpan(pos, 1), "- ") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ChangeRefCellDerefToNotExpression.fs b/vsintegration/src/FSharp.Editor/CodeFix/ChangeRefCellDerefToNotExpression.fs index 04b40ec52be..8937117cfd0 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ChangeRefCellDerefToNotExpression.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ChangeRefCellDerefToNotExpression.fs @@ -13,6 +13,7 @@ type internal FSharpChangeRefCellDerefToNotExpressionCodeFixProvider [ Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ChangeRefCellDerefToNotExpression, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(derefSpan, "not ") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ChangeRefCellDerefToNotExpression, title, [| TextChange(derefSpan, "not ") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ChangeToUpcast.fs b/vsintegration/src/FSharp.Editor/CodeFix/ChangeToUpcast.fs index d2e7faedc3c..16032747c5b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ChangeToUpcast.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ChangeToUpcast.fs @@ -43,20 +43,7 @@ type internal FSharpChangeToUpcastCodeFixProvider() = else SR.UseUpcastKeyword() - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ChangeToUpcast, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, replacement) |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ChangeToUpcast, title, [| TextChange(context.Span, replacement) |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFix/CodeFixHelpers.fs index 9c96071c018..e8af59ad674 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/CodeFixHelpers.fs @@ -2,8 +2,14 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Threading +open System.Threading.Tasks +open System.Collections.Immutable +open System.Diagnostics +open Microsoft +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.CodeActions @@ -11,63 +17,60 @@ open Microsoft.VisualStudio.FSharp.Editor.Telemetry [] module internal CodeFixHelpers = - let createTextChangeCodeFix - ( - name: string, - title: string, - context: CodeFixContext, - computeTextChanges: unit -> Async - ) = - - // Currently there should be one Id here. - // Keeping it this way to be error- and futureproof - // as the underlying API does allow multiple Ids here. - let ids = context.Diagnostics |> Seq.map (fun d -> d.Id) |> String.concat "," + let private reportCodeFixTelemetry (diagnostics: ImmutableArray) (doc: Document) (staticName: string) (additionalProps) = + let ids = + diagnostics |> Seq.map (fun d -> d.Id) |> Seq.distinct |> String.concat "," let props: (string * obj) list = - [ - "name", name + additionalProps + @ [ + "name", staticName "ids", ids - - // The following can help building a unique but anonymized codefix target: - // #projectid#documentid#span - // Then we can check if the codefix was actually activated after its creation. - "context.document.project.id", context.Document.Project.Id.Id.ToString() - "context.document.id", context.Document.Id.Id.ToString() - "context.span", context.Span.ToString() + "context.document.project.id", doc.Project.Id.Id.ToString() + "context.document.id", doc.Id.Id.ToString() + "context.diagnostics.count", diagnostics.Length ] - TelemetryReporter.reportEvent "codefixregistered" props + TelemetryReporter.reportEvent "codefixactivated" props + + let createFixAllProvider name getChanges = + FixAllProvider.Create(fun fixAllCtx doc allDiagnostics -> + backgroundTask { + let sw = Stopwatch.StartNew() + let! (changes: seq) = getChanges (doc, allDiagnostics, fixAllCtx.CancellationToken) + let! text = doc.GetTextAsync(fixAllCtx.CancellationToken) + let doc = doc.WithText(text.WithChanges(changes)) + + do + reportCodeFixTelemetry + allDiagnostics + doc + name + [ "scope", fixAllCtx.Scope.ToString(); "elapsedMs", sw.ElapsedMilliseconds ] + return doc + }) + + let createTextChangeCodeFix (name: string, title: string, context: CodeFixContext, changes: TextChange seq) = CodeAction.Create( title, (fun (cancellationToken: CancellationToken) -> - async { - let! sourceText = context.Document.GetTextAsync(cancellationToken) |> Async.AwaitTask - let! changesOpt = computeTextChanges () - - match changesOpt with - | None -> return context.Document - | Some textChanges -> - // Note: "activated" doesn't mean "applied". - // It's one step prior to that: - // e.g. when one clicks (Ctrl + .) and looks at the potential change. - TelemetryReporter.reportEvent "codefixactivated" props - return context.Document.WithText(sourceText.WithChanges(textChanges)) - } - |> RoslynHelpers.StartAsyncAsTask(cancellationToken)), - title + backgroundTask { + let! sourceText = context.Document.GetTextAsync(cancellationToken) + let doc = context.Document.WithText(sourceText.WithChanges(changes)) + reportCodeFixTelemetry context.Diagnostics context.Document name [] + return doc + }), + name ) [] module internal CodeFixExtensions = - type CodeFixProvider with - - member this.GetPrunedDiagnostics(context: CodeFixContext) = - context.Diagnostics.RemoveAll(fun x -> this.FixableDiagnosticIds.Contains(x.Id) |> not) + type CodeFixContext with - member this.RegisterFix(name, title, context: CodeFixContext, fixChange) = - let replaceCodeFix = - CodeFixHelpers.createTextChangeCodeFix (name, title, context, (fun () -> asyncMaybe.Return [| fixChange |])) + member ctx.RegisterFsharpFix(staticName, title, changes, ?diagnostics) = + let codeAction = + CodeFixHelpers.createTextChangeCodeFix (staticName, title, ctx, changes) - context.RegisterCodeFix(replaceCodeFix, this.GetPrunedDiagnostics(context)) + let diag = diagnostics |> Option.defaultValue ctx.Diagnostics + ctx.RegisterCodeFix(codeAction, diag) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpLambdaToFSharpLambda.fs b/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpLambdaToFSharpLambda.fs index 1c38dc2e021..aaff8595165 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpLambdaToFSharpLambda.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpLambdaToFSharpLambda.fs @@ -12,7 +12,7 @@ type internal FSharpConvertCSharpLambdaToFSharpLambdaCodeFixProvider [ " + bodyText) - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let title = SR.UseFSharpLambda() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ConvertCSharpLambdaToFSharpLambda, - title, - context, - (fun () -> asyncMaybe.Return [| replacement |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ConvertCSharpLambdaToFSharpLambda, title, [| replacement |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpUsingToFSharpOpen.fs b/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpUsingToFSharpOpen.fs index d92eea460a2..eba65241e98 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpUsingToFSharpOpen.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ConvertCSharpUsingToFSharpOpen.fs @@ -15,6 +15,7 @@ type internal FSharpConvertCSharpUsingToFSharpOpen [] () = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0039"; "FS0201" ] + static let title = SR.ConvertCSharpUsingToFSharpOpen() let usingLength = "using".Length let isCSharpUsingShapeWithPos (context: CodeFixContext) (sourceText: SourceText) = @@ -39,34 +40,17 @@ type internal FSharpConvertCSharpUsingToFSharpOpen [] () = let slice = sourceText.GetSubText(span).ToString() struct (slice = "using", start) - let registerCodeFix (context: CodeFixContext) (diagnostics: ImmutableArray) (str: string) (span: TextSpan) = + let registerCodeFix (context: CodeFixContext) (str: string) (span: TextSpan) = let replacement = let str = str.Replace("using", "open").Replace(";", "") TextChange(span, str) - let title = SR.ConvertCSharpUsingToFSharpOpen() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ConvertCSharpUsingToFSharpOpen, - title, - context, - (fun () -> asyncMaybe.Return [| replacement |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ConvertCSharpUsingToFSharpOpen, title, [| replacement |]) override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context = asyncMaybe { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - do! Option.guard (diagnostics.Length > 0) - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) // TODO: handle single-line case? @@ -83,7 +67,7 @@ type internal FSharpConvertCSharpUsingToFSharpOpen [] () = (statementWithSemicolon.StartsWith("using") && statementWithSemicolon.EndsWith(";")) then - registerCodeFix context diagnostics statementWithSemicolon statementWithSemicolonSpan + registerCodeFix context statementWithSemicolon statementWithSemicolonSpan else // Only the identifier being opened has a diagnostic, so we try to find the rest of the statement let struct (isCSharpUsingShape, start) = @@ -93,7 +77,7 @@ type internal FSharpConvertCSharpUsingToFSharpOpen [] () = let len = (context.Span.Start - start) + statementWithSemicolonSpan.Length let fullSpan = TextSpan(start, len) let str = sourceText.GetSubText(fullSpan).ToString() - registerCodeFix context diagnostics str fullSpan + registerCodeFix context str fullSpan } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ConvertToNotEqualsEqualityExpression.fs b/vsintegration/src/FSharp.Editor/CodeFix/ConvertToNotEqualsEqualityExpression.fs index f1304f3996f..21cb064378f 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ConvertToNotEqualsEqualityExpression.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ConvertToNotEqualsEqualityExpression.fs @@ -13,6 +13,7 @@ type internal FSharpConvertToNotEqualsEqualityExpressionCodeFixProvider() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0043" ] + static let title = SR.ConvertToNotEqualsEqualityExpression() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds @@ -24,23 +25,7 @@ type internal FSharpConvertToNotEqualsEqualityExpressionCodeFixProvider() = // We're converting '!=' into '<>', a common new user mistake. // If this is an FS00043 that is anything other than that, bail out do! Option.guard (text = "!=") - - let title = SR.ConvertToNotEqualsEqualityExpression() - - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ConvertToNotEqualsEqualityExpression, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, "<>") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ConvertToNotEqualsEqualityExpression, title, [| TextChange(context.Span, "<>") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ConvertToSingleEqualsEqualityExpression.fs b/vsintegration/src/FSharp.Editor/CodeFix/ConvertToSingleEqualsEqualityExpression.fs index 6832851a189..0a8defff17d 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ConvertToSingleEqualsEqualityExpression.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ConvertToSingleEqualsEqualityExpression.fs @@ -13,6 +13,7 @@ type internal FSharpConvertToSingleEqualsEqualityExpressionCodeFixProvider() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0043" ] + static let title = SR.ConvertToSingleEqualsEqualityExpression() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds @@ -24,23 +25,7 @@ type internal FSharpConvertToSingleEqualsEqualityExpressionCodeFixProvider() = // We're converting '==' into '=', a common new user mistake. // If this is an FS00043 that is anything other than that, bail out do! Option.guard (text = "==") - - let title = SR.ConvertToSingleEqualsEqualityExpression() - - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.ConvertToSingleEqualsEqualityExpression, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, "=") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.ConvertToSingleEqualsEqualityExpression, title, [| TextChange(context.Span, "=") |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/FixIndexerAccess.fs b/vsintegration/src/FSharp.Editor/CodeFix/FixIndexerAccess.fs index c594e86afd6..9c10a6e0552 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/FixIndexerAccess.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/FixIndexerAccess.fs @@ -4,8 +4,10 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition open System.Collections.Immutable +open System.Threading open System.Threading.Tasks +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes open FSharp.Compiler.Diagnostics @@ -14,48 +16,40 @@ open FSharp.Compiler.Diagnostics type internal LegacyFsharpFixAddDotToIndexerAccess() = inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS3217" ] + static let title = CompilerDiagnostics.GetErrorMessage FSharpDiagnosticKind.AddIndexerDot override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context : Task = async { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toList - - if not (List.isEmpty diagnostics) then - let! sourceText = context.Document.GetTextAsync() |> Async.AwaitTask - - diagnostics - |> Seq.iter (fun diagnostic -> - let diagnostics = ImmutableArray.Create diagnostic - - let span, replacement = - try - let mutable span = context.Span - - let notStartOfBracket (span: TextSpan) = - let t = sourceText.GetSubText(TextSpan(span.Start, span.Length + 1)) - t.[t.Length - 1] <> '[' - - // skip all braces and blanks until we find [ - while span.End < sourceText.Length && notStartOfBracket span do - span <- TextSpan(span.Start, span.Length + 1) - - span, sourceText.GetSubText(span).ToString() - with _ -> - context.Span, sourceText.GetSubText(context.Span).ToString() - - let codefix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.FixIndexerAccess, - CompilerDiagnostics.GetErrorMessage FSharpDiagnosticKind.AddIndexerDot, - context, - (fun () -> asyncMaybe.Return [| TextChange(span, replacement.TrimEnd() + ".") |]) - ) - - context.RegisterCodeFix(codefix, diagnostics)) + let! sourceText = context.Document.GetTextAsync() |> Async.AwaitTask + + context.Diagnostics + |> Seq.iter (fun diagnostic -> + + let span, replacement = + try + let mutable span = context.Span + + let notStartOfBracket (span: TextSpan) = + let t = sourceText.GetSubText(TextSpan(span.Start, span.Length + 1)) + t.[t.Length - 1] <> '[' + + // skip all braces and blanks until we find [ + while span.End < sourceText.Length && notStartOfBracket span do + span <- TextSpan(span.Start, span.Length + 1) + + span, sourceText.GetSubText(span).ToString() + with _ -> + context.Span, sourceText.GetSubText(context.Span).ToString() + + do + context.RegisterFsharpFix( + CodeFix.FixIndexerAccess, + title, + [| TextChange(span, replacement.TrimEnd() + ".") |], + ImmutableArray.Create(diagnostic) + )) } |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) @@ -67,12 +61,21 @@ type internal FsharpFixRemoveDotFromIndexerAccessOptIn() as this = static let title = CompilerDiagnostics.GetErrorMessage FSharpDiagnosticKind.RemoveIndexerDot + member this.GetChanges(_document: Document, diagnostics: ImmutableArray, _ct: CancellationToken) = + backgroundTask { + let changes = + diagnostics |> Seq.map (fun x -> TextChange(x.Location.SourceSpan, "")) + + return changes + } + override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds - override _.RegisterCodeFixesAsync context : Task = + override _.RegisterCodeFixesAsync ctx : Task = backgroundTask { - let relevantDiagnostics = this.GetPrunedDiagnostics(context) - - if not relevantDiagnostics.IsEmpty then - this.RegisterFix(CodeFix.RemoveIndexerDotBeforeBracket, title, context, TextChange(context.Span, "")) + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.RemoveIndexerDotBeforeBracket, title, changes) } + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.RemoveIndexerDotBeforeBracket this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/MakeDeclarationMutable.fs b/vsintegration/src/FSharp.Editor/CodeFix/MakeDeclarationMutable.fs index aefbc74be70..5285b20007c 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/MakeDeclarationMutable.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/MakeDeclarationMutable.fs @@ -16,15 +16,12 @@ type internal FSharpMakeDeclarationMutableFixProvider [] ( inherit CodeFixProvider() let fixableDiagnosticIds = set [ "FS0027" ] + static let title = SR.MakeDeclarationMutable() override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds override _.RegisterCodeFixesAsync context : Task = asyncMaybe { - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray let document = context.Document do! Option.guard (not (isSignatureFile document.FilePath)) @@ -65,18 +62,7 @@ type internal FSharpMakeDeclarationMutableFixProvider [] ( // Bail if it's a parameter, because like, that ain't allowed do! Option.guard (not (parseFileResults.IsPositionContainedInACurriedParameter declRange.Start)) - - let title = SR.MakeDeclarationMutable() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.MakeDeclarationMutable, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(span.Start, 0), "mutable ") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.MakeDeclarationMutable, title, [| TextChange(TextSpan(span.Start, 0), "mutable ") |]) | _ -> () } |> Async.Ignore diff --git a/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs b/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs index bae864afebb..7f0ae479743 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs @@ -40,23 +40,15 @@ type internal FSharpMakeOuterBindingRecursiveCodeFixProvider [ Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - let title = String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(outerBindingNameSpan).ToString()) - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( + do + context.RegisterFsharpFix( CodeFix.MakeOuterBindingRecursive, title, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(outerBindingNameSpan.Start, 0), "rec ") |]) + [| TextChange(TextSpan(outerBindingNameSpan.Start, 0), "rec ") |] ) - - context.RegisterCodeFix(codeFix, diagnostics) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RemoveReturnOrYield.fs b/vsintegration/src/FSharp.Editor/CodeFix/RemoveReturnOrYield.fs index a002916639d..7e76fc0003a 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RemoveReturnOrYield.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RemoveReturnOrYield.fs @@ -29,11 +29,6 @@ type internal FSharpRemoveReturnOrYieldCodeFixProvider [] let! exprRange = parseResults.TryRangeOfExprInYieldOrReturn errorRange.Start let! exprSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, exprRange) - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - let title = let text = sourceText.GetSubText(context.Span).ToString() @@ -42,15 +37,12 @@ type internal FSharpRemoveReturnOrYieldCodeFixProvider [] elif text.StartsWith("yield!") then SR.RemoveYieldBang() else SR.RemoveYield() - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( + do + context.RegisterFsharpFix( CodeFix.RemoveReturnOrYield, title, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, sourceText.GetSubText(exprSpan).ToString()) |]) + [| TextChange(context.Span, sourceText.GetSubText(exprSpan).ToString()) |] ) - - context.RegisterCodeFix(codeFix, diagnostics) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RemoveSuperflousCaptureForUnionCaseWithNoData.fs b/vsintegration/src/FSharp.Editor/CodeFix/RemoveSuperflousCaptureForUnionCaseWithNoData.fs index bc49d1f13d3..d2fcfa51b19 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RemoveSuperflousCaptureForUnionCaseWithNoData.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RemoveSuperflousCaptureForUnionCaseWithNoData.fs @@ -3,8 +3,12 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeActions open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes @@ -15,38 +19,47 @@ type internal RemoveSuperflousCaptureForUnionCaseWithNoDataProvider [, ct: CancellationToken) = + backgroundTask { - let document = context.Document - let! sourceText = document.GetTextAsync(context.CancellationToken) + let! sourceText = document.GetTextAsync(ct) + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(CodeFix.RemoveSuperfluousCapture) - let! _, checkResults = - document.GetFSharpParseAndCheckResultsAsync(nameof (RemoveSuperflousCaptureForUnionCaseWithNoDataProvider)) - |> liftAsync + let changes = + seq { + for d in diagnostics do + let textSpan = d.Location.SourceSpan + let m = RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) + let classifications = checkResults.GetSemanticClassification(Some m) - let m = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, context.Span, sourceText) + let unionCaseItem = + classifications + |> Array.tryFind (fun c -> c.Type = SemanticClassificationType.UnionCase) - let classifications = checkResults.GetSemanticClassification(Some m) + match unionCaseItem with + | None -> () + | Some unionCaseItem -> + // The error/warning captures entire pattern match, like "Ns.Type.DuName bindingName". We want to keep type info when suggesting a replacement, and only remove "bindingName". + let typeInfoLength = unionCaseItem.Range.EndColumn - m.StartColumn - let unionCaseItem = - classifications - |> Array.tryFind (fun c -> c.Type = SemanticClassificationType.UnionCase) + let reminderSpan = + new TextSpan(textSpan.Start + typeInfoLength, textSpan.Length - typeInfoLength) - match unionCaseItem with - | None -> () - | Some unionCaseItem -> - // The error/warning captures entire pattern match, like "Ns.Type.DuName bindingName". We want to keep type info when suggesting a replacement, and only remove "bindingName". - let typeInfoLength = unionCaseItem.Range.EndColumn - m.StartColumn + yield TextChange(reminderSpan, "") + } - let reminderSpan = - new TextSpan(context.Span.Start + typeInfoLength, context.Span.Length - typeInfoLength) + return changes + } - this.RegisterFix(CodeFix.RemoveSuperfluousCapture, SR.RemoveUnusedBinding(), context, TextChange(reminderSpan, "")) + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + if ctx.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled then + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.RemoveSuperfluousCapture, title, changes) } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.RemoveSuperfluousCapture this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedBinding.fs b/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedBinding.fs index e290cb7173e..9335eda0000 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedBinding.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedBinding.fs @@ -4,67 +4,71 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeActions open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open FSharp.Compiler.EditorServices + [] type internal FSharpRemoveUnusedBindingCodeFixProvider [] () = inherit CodeFixProvider() - let fixableDiagnosticIds = set [ "FS1182" ] - - override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds - - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - // Don't show code fixes for unused values, even if they are compiler-generated. - do! Option.guard context.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled + static let title = SR.RemoveUnusedBinding() + override _.FixableDiagnosticIds = ImmutableArray.Create("FS1182") - let document = context.Document - let! sourceText = document.GetTextAsync(context.CancellationToken) + member this.GetChanges(document: Document, diagnostics: ImmutableArray, ct: CancellationToken) = + backgroundTask { - let! parseResults = - context.Document.GetFSharpParseResultsAsync(nameof (FSharpRemoveUnusedBindingCodeFixProvider)) - |> liftAsync + let! sourceText = document.GetTextAsync(ct) + let! parseResults = document.GetFSharpParseResultsAsync(nameof (FSharpRemoveUnusedBindingCodeFixProvider)) - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray + let changes = + seq { + for d in diagnostics do + let textSpan = d.Location.SourceSpan - let symbolRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, context.Span, sourceText) + let symbolRange = + RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - let! rangeOfBinding = parseResults.TryRangeOfBindingWithHeadPatternWithPos(symbolRange.Start) - let! spanOfBinding = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, rangeOfBinding) + let spanOfBindingOpt = + parseResults.TryRangeOfBindingWithHeadPatternWithPos(symbolRange.Start) + |> Option.bind (fun rangeOfBinding -> RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, rangeOfBinding)) - let keywordEndColumn = - let rec loop ch pos = - if not (Char.IsWhiteSpace(ch)) then - pos - else - loop sourceText.[pos - 1] (pos - 1) + match spanOfBindingOpt with + | Some spanOfBinding -> + let keywordEndColumn = + let rec loop ch pos = + if not (Char.IsWhiteSpace(ch)) then + pos + else + loop sourceText.[pos - 1] (pos - 1) - loop sourceText.[spanOfBinding.Start - 1] (spanOfBinding.Start - 1) + loop sourceText.[spanOfBinding.Start - 1] (spanOfBinding.Start - 1) - // This is safe, since we could never have gotten here unless there was a `let` or `use` - let keywordStartColumn = keywordEndColumn - 2 - let fullSpan = TextSpan(keywordStartColumn, spanOfBinding.End - keywordStartColumn) + // This is safe, since we could never have gotten here unless there was a `let` or `use` + let keywordStartColumn = keywordEndColumn - 2 + let fullSpan = TextSpan(keywordStartColumn, spanOfBinding.End - keywordStartColumn) - let prefixTitle = SR.RemoveUnusedBinding() + yield TextChange(fullSpan, "") + | None -> () + } - let removalCodeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.RemoveUnusedBinding, - prefixTitle, - context, - (fun () -> asyncMaybe.Return [| TextChange(fullSpan, "") |]) - ) + return changes + } - context.RegisterCodeFix(removalCodeFix, diagnostics) + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + if ctx.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled then + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.RemoveUnusedBinding, title, changes) } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.RemoveUnusedBinding this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedOpens.fs b/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedOpens.fs index f46e1018f49..b57f35bd61b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedOpens.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RemoveUnusedOpens.fs @@ -3,10 +3,14 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.CodeActions open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open FSharp.Compiler.Text @@ -15,37 +19,35 @@ open FSharp.Compiler.Text type internal FSharpRemoveUnusedOpensCodeFixProvider [] () = inherit CodeFixProvider() - let fixableDiagnosticIds = - [ FSharpIDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId ] + static let title = SR.RemoveUnusedOpens() - override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds + override _.FixableDiagnosticIds = + ImmutableArray.Create FSharpIDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - let document = context.Document - let! sourceText = document.GetTextAsync() - let! unusedOpens = UnusedOpensDiagnosticAnalyzer.GetUnusedOpenRanges(document) + member this.GetChanges(document: Document, diagnostics: ImmutableArray, ct: CancellationToken) = + backgroundTask { + let! sourceText = document.GetTextAsync(ct) let changes = - unusedOpens - |> List.map (fun m -> - let span = sourceText.Lines.[Line.toZ m.StartLine].SpanIncludingLineBreak - TextChange(span, "")) - |> List.toArray - - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id) - |> Seq.toImmutableArray - - let title = SR.RemoveUnusedOpens() - - let codefix = - CodeFixHelpers.createTextChangeCodeFix (CodeFix.RemoveUnusedOpens, title, context, (fun () -> asyncMaybe.Return changes)) + diagnostics + |> Seq.map (fun d -> + sourceText + .Lines + .GetLineFromPosition( + d.Location.SourceSpan.Start + ) + .SpanIncludingLineBreak) + |> Seq.map (fun span -> TextChange(span, "")) + + return changes + } - context.RegisterCodeFix(codefix, diagnostics) + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + if ctx.Document.Project.IsFSharpCodeFixesUnusedOpensEnabled then + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.RemoveUnusedOpens, title, changes) } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) - override _.GetFixAllProvider() = WellKnownFixAllProviders.BatchFixer + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.RemoveUnusedOpens this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RenameParamToMatchSignature.fs b/vsintegration/src/FSharp.Editor/CodeFix/RenameParamToMatchSignature.fs index 68ca143aef5..fb916fbb849 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RenameParamToMatchSignature.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RenameParamToMatchSignature.fs @@ -2,14 +2,18 @@ namespace Microsoft.VisualStudio.FSharp.Editor -open System.Collections.Immutable open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open System.Text.RegularExpressions +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes - +open Microsoft.CodeAnalysis.CodeActions open Microsoft.VisualStudio.FSharp.Editor.SymbolHelpers + open FSharp.Compiler.Diagnostics open FSharp.Compiler.Tokenization.FSharpKeywords @@ -18,57 +22,61 @@ type internal FSharpRenameParamToMatchSignature [] () = inherit CodeFixProvider() - let fixableDiagnosticIds = [ "FS3218" ] - - override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds - - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - match - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id) - |> Seq.toList - with - | [ diagnostic ] -> - let message = diagnostic.GetMessage() - - let parts = - System.Text.RegularExpressions.Regex.Match(message, ".+'(.+)'.+'(.+)'.+") - - if parts.Success then - - let diagnostics = ImmutableArray.Create diagnostic - let suggestion = parts.Groups.[1].Value - let replacement = NormalizeIdentifierBackticks suggestion - - let computeChanges () = - asyncMaybe { - let document = context.Document - let! cancellationToken = Async.CancellationToken |> liftAsync - let! sourceText = document.GetTextAsync(cancellationToken) - let! symbolUses = getSymbolUsesOfSymbolAtLocationInDocument (document, context.Span.Start) - - let changes = - [| - for symbolUse in symbolUses do - match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) with - | None -> () - | Some span -> - let textSpan = Tokenizer.fixupSpan (sourceText, span) - yield TextChange(textSpan, replacement) - |] - - return changes - } - - let title = - CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion suggestion) - - let codefix = - CodeFixHelpers.createTextChangeCodeFix (CodeFix.FSharpRenameParamToMatchSignature, title, context, computeChanges) - - context.RegisterCodeFix(codefix, diagnostics) - | _ -> () + let getSuggestion (d: Diagnostic) = + let parts = Regex.Match(d.GetMessage(), ".+'(.+)'.+'(.+)'.+") + + if parts.Success then + ValueSome parts.Groups.[1].Value + else + ValueNone + + override _.FixableDiagnosticIds = ImmutableArray.Create("FS3218") + + member this.GetChanges(document: Document, diagnostics: ImmutableArray, ct: CancellationToken) = + backgroundTask { + let! sourceText = document.GetTextAsync(ct) + + let! changes = + seq { + for d in diagnostics do + let suggestionOpt = getSuggestion d + + match suggestionOpt with + | ValueSome suggestion -> + let replacement = NormalizeIdentifierBackticks suggestion + + async { + let! symbolUses = getSymbolUsesOfSymbolAtLocationInDocument (document, d.Location.SourceSpan.Start) + let symbolUses = symbolUses |> Option.defaultValue [||] + + return + [| + for symbolUse in symbolUses do + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) with + | None -> () + | Some span -> + let textSpan = Tokenizer.fixupSpan (sourceText, span) + yield TextChange(textSpan, replacement) + |] + + } + | ValueNone -> () + } + |> Async.Parallel + + return (changes |> Seq.concat) } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + let title = ctx.Diagnostics |> Seq.head |> getSuggestion + + match title with + | ValueSome title -> + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.FSharpRenameParamToMatchSignature, title, changes) + | ValueNone -> () + } + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.FSharpRenameParamToMatchSignature this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/RenameUnusedValue.fs b/vsintegration/src/FSharp.Editor/CodeFix/RenameUnusedValue.fs index 06a99d9538f..63f8c0af545 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/RenameUnusedValue.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/RenameUnusedValue.fs @@ -4,90 +4,162 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Composition +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols open FSharp.Compiler.Syntax -[] -type internal FSharpRenameUnusedValueCodeFixProvider [] () = +module UnusedCodeFixHelper = + let getUnusedSymbol (sourceText: SourceText) (textSpan: TextSpan) (document: Document) = - inherit CodeFixProvider() + let ident = sourceText.ToString(textSpan) - let fixableDiagnosticIds = set [ "FS1182" ] + // Prefixing operators and backticked identifiers does not make sense. + // We have to use the additional check for backtickes + if PrettyNaming.IsIdentifierName ident then + asyncMaybe { + let! lexerSymbol = + document.TryFindFSharpLexerSymbolAsync(textSpan.Start, SymbolLookupKind.Greedy, false, false, CodeFix.RenameUnusedValue) - override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds + let m = RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - // Don't show code fixes for unused values, even if they are compiler-generated. - do! Option.guard context.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled + let lineText = (sourceText.Lines.GetLineFromPosition textSpan.Start).ToString() - let document = context.Document - let! sourceText = document.GetTextAsync(context.CancellationToken) - let ident = sourceText.ToString(context.Span) + let! _, checkResults = + document.GetFSharpParseAndCheckResultsAsync(CodeFix.RenameUnusedValue) + |> liftAsync - // Prefixing operators and backticked identifiers does not make sense. - // We have to use the additional check for backtickes - if PrettyNaming.IsIdentifierName ident then - let! lexerSymbol = - document.TryFindFSharpLexerSymbolAsync( - context.Span.Start, - SymbolLookupKind.Greedy, - false, - false, - nameof (FSharpRenameUnusedValueCodeFixProvider) - ) + return! checkResults.GetSymbolUseAtLocation(m.StartLine, m.EndColumn, lineText, lexerSymbol.FullIsland) - let m = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, context.Span, sourceText) + } + else + async { return None } - let lineText = (sourceText.Lines.GetLineFromPosition context.Span.Start).ToString() +[] +type internal FSharpPrefixUnusedValueWithUnderscoreCodeFixProvider [] () = - let! _, checkResults = - document.GetFSharpParseAndCheckResultsAsync(nameof (FSharpRenameUnusedValueCodeFixProvider)) - |> liftAsync + inherit CodeFixProvider() + + static let title (symbolName: string) = + String.Format(SR.PrefixValueNameWithUnderscore(), symbolName) + + override _.FixableDiagnosticIds = ImmutableArray.Create("FS1182") + + member this.GetChanges(document: Document, diagnostics: ImmutableArray, ct: CancellationToken) = + backgroundTask { + let! sourceText = document.GetTextAsync(ct) + + let! changes = + seq { + for d in diagnostics do + let textSpan = d.Location.SourceSpan + + yield + async { + let! symbolUse = UnusedCodeFixHelper.getUnusedSymbol sourceText textSpan document + + return + seq { + match symbolUse with + | None -> () + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue -> yield TextChange(TextSpan(textSpan.Start, 0), "_") + | _ -> () + } + } + } + |> Async.Parallel + + return (changes |> Seq.concat) + } - let! symbolUse = checkResults.GetSymbolUseAtLocation(m.StartLine, m.EndColumn, lineText, lexerSymbol.FullIsland) - let symbolName = symbolUse.Symbol.DisplayName - - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as func -> - let prefixTitle = String.Format(SR.PrefixValueNameWithUnderscore(), symbolName) - - let prefixCodeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.RenameUnusedValue, - prefixTitle, - context, - (fun () -> asyncMaybe.Return [| TextChange(TextSpan(context.Span.Start, 0), "_") |]) - ) - - context.RegisterCodeFix(prefixCodeFix, diagnostics) - - if func.IsValue then - let replaceTitle = String.Format(SR.RenameValueToUnderscore(), symbolName) - - let replaceCodeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.RenameUnusedValue, - replaceTitle, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, "_") |]) - ) - - context.RegisterCodeFix(replaceCodeFix, diagnostics) - | _ -> () + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + if ctx.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled then + let! sourceText = ctx.Document.GetTextAsync(ctx.CancellationToken) + let! unusedSymbol = UnusedCodeFixHelper.getUnusedSymbol sourceText ctx.Span ctx.Document + + match unusedSymbol with + | None -> () + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue -> + let prefixTitle = title symbolUse.Symbol.DisplayName + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.PrefixUnusedValue, prefixTitle, changes) + | _ -> () } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.PrefixUnusedValue this.GetChanges + +[] +type internal FSharpRenameUnusedValueWithUnderscoreCodeFixProvider [] () = + + inherit CodeFixProvider() + + static let title (symbolName: string) = + String.Format(SR.RenameValueToUnderscore(), symbolName) + + override _.FixableDiagnosticIds = ImmutableArray.Create("FS1182") + + member this.GetChanges(document: Document, diagnostics: ImmutableArray, ct: CancellationToken) = + backgroundTask { + let! sourceText = document.GetTextAsync(ct) + + let! changes = + seq { + for d in diagnostics do + let textSpan = d.Location.SourceSpan + + yield + async { + let! symbolUse = UnusedCodeFixHelper.getUnusedSymbol sourceText textSpan document + + return + seq { + match symbolUse with + | None -> () + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as func when func.IsValue -> yield TextChange(textSpan, "_") + | _ -> () + } + } + } + |> Async.Parallel + + return (changes |> Seq.concat) + } + + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + if ctx.Document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled then + let! sourceText = ctx.Document.GetTextAsync(ctx.CancellationToken) + let! unusedSymbol = UnusedCodeFixHelper.getUnusedSymbol sourceText ctx.Span ctx.Document + + match unusedSymbol with + | None -> () + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as func when func.IsValue -> + let prefixTitle = title symbolUse.Symbol.DisplayName + + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.RenameUnusedValue, prefixTitle, changes) + | _ -> () + } + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.RenameUnusedValue this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ReplaceWithSuggestion.fs b/vsintegration/src/FSharp.Editor/CodeFix/ReplaceWithSuggestion.fs index b6c53999150..f192a0f573b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ReplaceWithSuggestion.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ReplaceWithSuggestion.fs @@ -51,23 +51,15 @@ type internal FSharpReplaceWithSuggestionCodeFixProvider [ for item in declInfo.Items do addToBuffer item.NameInList - let diagnostics = - context.Diagnostics - |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - for suggestion in CompilerDiagnostics.GetSuggestedNames addNames unresolvedIdentifierText do let replacement = PrettyNaming.NormalizeIdentifierBackticks suggestion - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( + do + context.RegisterFsharpFix( CodeFix.ReplaceWithSuggestion, CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion suggestion), - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, replacement) |]) + [| TextChange(context.Span, replacement) |] ) - - context.RegisterCodeFix(codeFix, diagnostics) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/SimplifyName.fs b/vsintegration/src/FSharp.Editor/CodeFix/SimplifyName.fs index 34bbfab9d93..dae2e3095a5 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/SimplifyName.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/SimplifyName.fs @@ -3,36 +3,45 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System.Composition -open System.Collections.Immutable +open System.Threading open System.Threading.Tasks +open System.Collections.Immutable +open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.CodeActions open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics +open FSharp.Compiler.Text + [] type internal FSharpSimplifyNameCodeFixProvider() = inherit CodeFixProvider() - let fixableDiagnosticId = FSharpIDEDiagnosticIds.SimplifyNamesDiagnosticId - - override _.FixableDiagnosticIds = ImmutableArray.Create(fixableDiagnosticId) - - override _.RegisterCodeFixesAsync(context: CodeFixContext) : Task = - async { - for diagnostic in context.Diagnostics |> Seq.filter (fun x -> x.Id = fixableDiagnosticId) do - let title = - match diagnostic.Properties.TryGetValue(SimplifyNameDiagnosticAnalyzer.LongIdentPropertyKey) with - | true, longIdent -> sprintf "%s '%s'" (SR.SimplifyName()) longIdent - | _ -> SR.SimplifyName() - - let codefix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.SimplifyName, - title, - context, - (fun () -> asyncMaybe.Return [| TextChange(context.Span, "") |]) - ) - - context.RegisterCodeFix(codefix, ImmutableArray.Create(diagnostic)) + + override _.FixableDiagnosticIds = + ImmutableArray.Create(FSharpIDEDiagnosticIds.SimplifyNamesDiagnosticId) + + member this.GetChanges(_document: Document, diagnostics: ImmutableArray, _ct: CancellationToken) = + backgroundTask { + let changes = + diagnostics |> Seq.map (fun d -> TextChange(d.Location.SourceSpan, "")) + + return changes } - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + + override this.RegisterCodeFixesAsync ctx : Task = + backgroundTask { + let diag = ctx.Diagnostics |> Seq.head + + let title = + match diag.Properties.TryGetValue(SimplifyNameDiagnosticAnalyzer.LongIdentPropertyKey) with + | true, longIdent -> sprintf "%s '%s'" (SR.SimplifyName()) longIdent + | _ -> SR.SimplifyName() + + let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken) + ctx.RegisterFsharpFix(CodeFix.SimplifyName, title, changes) + } + + override this.GetFixAllProvider() = + CodeFixHelpers.createFixAllProvider CodeFix.SimplifyName this.GetChanges diff --git a/vsintegration/src/FSharp.Editor/CodeFix/UseMutationWhenValueIsMutable.fs b/vsintegration/src/FSharp.Editor/CodeFix/UseMutationWhenValueIsMutable.fs index f1358f569dc..0bf89b2a2b7 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/UseMutationWhenValueIsMutable.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/UseMutationWhenValueIsMutable.fs @@ -17,16 +17,11 @@ type internal FSharpUseMutationWhenValueIsMutableFixProvider [ Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) - |> Seq.toImmutableArray - let document = context.Document do! Option.guard (not (isSignatureFile document.FilePath)) @@ -68,7 +63,6 @@ type internal FSharpUseMutationWhenValueIsMutableFixProvider [ - let title = SR.UseMutationWhenValueIsMutable() let! symbolSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) let mutable pos = symbolSpan.End let mutable ch = sourceText.[pos] @@ -78,15 +72,7 @@ type internal FSharpUseMutationWhenValueIsMutableFixProvider [ asyncMaybe.Return [| TextChange(TextSpan(pos + 1, 1), "<-") |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.UseMutationWhenValueIsMutable, title, [| TextChange(TextSpan(pos + 1, 1), "<-") |]) | _ -> () } |> Async.Ignore diff --git a/vsintegration/src/FSharp.Editor/CodeFix/UseTripleQuotedInterpolation.fs b/vsintegration/src/FSharp.Editor/CodeFix/UseTripleQuotedInterpolation.fs index 9249c01088f..3748ae53e6b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/UseTripleQuotedInterpolation.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/UseTripleQuotedInterpolation.fs @@ -12,7 +12,7 @@ type internal FSharpUseTripleQuotedInterpolationCodeFixProvider [ Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id) - |> Seq.toImmutableArray - - let title = SR.UseTripleQuotedInterpolation() - - let codeFix = - CodeFixHelpers.createTextChangeCodeFix ( - CodeFix.UseTripleQuotedInterpolation, - title, - context, - (fun () -> asyncMaybe.Return [| replacement |]) - ) - - context.RegisterCodeFix(codeFix, diagnostics) + do context.RegisterFsharpFix(CodeFix.UseTripleQuotedInterpolation, title, [| replacement |]) } |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Common/Constants.fs b/vsintegration/src/FSharp.Editor/Common/Constants.fs index b2585dc3272..4a31072444a 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -125,6 +125,9 @@ module internal CodeFix = [] let RenameUnusedValue = "RenameUnusedValue" + [] + let PrefixUnusedValue = "PrefixUnusedValue" + [] let FixIndexerAccess = "FixIndexerAccess"