diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index 0a54de176dd..22255af1614 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -82,8 +82,7 @@ module internal CodeFixHelpers = reportCodeFixTelemetry context.Diagnostics context.Document codeFix.Name [||] return doc } - |> CancellableTask.start cancellationToken), - codeFix.Name + |> CancellableTask.start cancellationToken) ) [] @@ -100,6 +99,16 @@ module internal CodeFixExtensions = } |> CancellableTask.startAsTask ctx.CancellationToken + member ctx.RegisterFsharpFixes(codeFix: IFSharpMultiCodeFixProvider) = + cancellableTask { + let! codeFixes = codeFix.GetCodeFixesAsync ctx + + for codeFix in codeFixes do + let codeAction = CodeFixHelpers.createTextChangeCodeFix (codeFix, ctx) + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics) + } + |> CancellableTask.startAsTask ctx.CancellationToken + member ctx.GetSourceTextAsync() = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs b/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs index 6202e71a6fa..8b64382095e 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs @@ -14,5 +14,10 @@ type FSharpCodeFix = Changes: TextChange list } +/// Provider can generate at most 1 suggestion. type IFSharpCodeFixProvider = abstract member GetCodeFixIfAppliesAsync: context: CodeFixContext -> CancellableTask + +/// Provider can generate multiple suggestions. +type IFSharpMultiCodeFixProvider = + abstract member GetCodeFixesAsync: context: CodeFixContext -> CancellableTask diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ReplaceWithSuggestion.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ReplaceWithSuggestion.fs index 3807848630e..31b0b84d374 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ReplaceWithSuggestion.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ReplaceWithSuggestion.fs @@ -23,12 +23,12 @@ type internal ReplaceWithSuggestionCodeFixProvider [] () = override this.RegisterCodeFixesAsync context = if context.Document.Project.IsFSharpCodeFixesSuggestNamesForErrorsEnabled then - context.RegisterFsharpFix this + context.RegisterFsharpFixes this else Task.CompletedTask - interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync context = + interface IFSharpMultiCodeFixProvider with + member _.GetCodeFixesAsync context = cancellableTask { let! parseFileResults, checkFileResults = context.Document.GetFSharpParseAndCheckResultsAsync(nameof ReplaceWithSuggestionCodeFixProvider) @@ -49,20 +49,14 @@ type internal ReplaceWithSuggestionCodeFixProvider [] () = for item in declInfo.Items do addToBuffer item.NameInList - let suggestionOpt = + return CompilerDiagnostics.GetSuggestedNames addNames unresolvedIdentifierText - |> Seq.tryHead - - match suggestionOpt with - | None -> return ValueNone - | Some suggestion -> - let replacement = PrettyNaming.NormalizeIdentifierBackticks suggestion - - return - ValueSome - { - Name = CodeFix.ReplaceWithSuggestion - Message = CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion suggestion) - Changes = [ TextChange(context.Span, replacement) ] - } + |> Seq.map (fun suggestion -> + let replacement = PrettyNaming.NormalizeIdentifierBackticks suggestion + + { + Name = CodeFix.ReplaceWithSuggestion + Message = CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion suggestion) + Changes = [ TextChange(context.Span, replacement) ] + }) } diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs index d1986e6ded3..88db55e34df 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs @@ -56,11 +56,13 @@ let getRelevantDiagnostics (document: Document) = |> CancellableTask.startWithoutCancellation |> fun task -> task.Result -let createTestCodeFixContext (code: string) document (mode: Mode) diagnosticIds = +let createTestCodeFixContext (code: string) (mode: Mode) (fixProvider: 'T :> CodeFixProvider) = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken () let sourceText = SourceText.From code + let document = getDocument code mode + let diagnosticIds = fixProvider.FixableDiagnosticIds let diagnostics = match mode with @@ -92,12 +94,11 @@ let createTestCodeFixContext (code: string) document (mode: Mode) diagnosticIds return CodeFixContext(document, textSpan, roslynDiagnostics, mockAction, cancellationToken) } -let tryFix (code: string) mode (fixProvider: 'T when 'T :> IFSharpCodeFixProvider and 'T :> CodeFixProvider) = +let tryFix (code: string) mode (fixProvider: 'T :> IFSharpCodeFixProvider) = cancellableTask { let sourceText = SourceText.From code - let document = getDocument code mode - let! context = createTestCodeFixContext code document mode fixProvider.FixableDiagnosticIds + let! context = createTestCodeFixContext code mode fixProvider let! result = fixProvider.GetCodeFixIfAppliesAsync context @@ -112,3 +113,22 @@ let tryFix (code: string) mode (fixProvider: 'T when 'T :> IFSharpCodeFixProvide } |> CancellableTask.startWithoutCancellation |> fun task -> task.Result + +let multiFix (code: string) mode (fixProvider: 'T :> IFSharpMultiCodeFixProvider) = + cancellableTask { + let sourceText = SourceText.From code + + let! context = createTestCodeFixContext code mode fixProvider + + let! result = fixProvider.GetCodeFixesAsync context + + return + result + |> Seq.map (fun codeFix -> + { + Message = codeFix.Message + FixedCode = (sourceText.WithChanges codeFix.Changes).ToString() + }) + } + |> CancellableTask.startWithoutCancellation + |> fun task -> task.Result diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ReplaceWithSuggestionTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ReplaceWithSuggestionTests.fs index 36ab31ef9f8..20495583a37 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ReplaceWithSuggestionTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ReplaceWithSuggestionTests.fs @@ -19,7 +19,7 @@ let song = { Titel = "Jigsaw Falling Into Place" } """ let expected = - Some + [ { Message = "Replace with 'Title'" FixedCode = @@ -29,8 +29,9 @@ type Song = { Title : string } let song = { Title = "Jigsaw Falling Into Place" } """ } + ] - let actual = codeFix |> tryFix code Auto + let actual = codeFix |> multiFix code Auto Assert.Equal(expected, actual) @@ -44,7 +45,7 @@ let someSong : Wrong = { Title = "The Narcissist" } """ let expected = - Some + [ { Message = "Replace with 'Song'" FixedCode = @@ -54,8 +55,47 @@ type Song = { Title : string } let someSong : Song = { Title = "The Narcissist" } """ } + ] - let actual = codeFix |> tryFix code Auto + let actual = codeFix |> multiFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Fixes FS0039 - multiple suggestions`` () = + let code = + """ +type TheType1() = class end +type TheType3() = class end + +let test = TheType2() +""" + + let expected = + [ + { + Message = "Replace with 'TheType1'" + FixedCode = + """ +type TheType1() = class end +type TheType3() = class end + +let test = TheType1() +""" + } + { + Message = "Replace with 'TheType3'" + FixedCode = + """ +type TheType1() = class end +type TheType3() = class end + +let test = TheType3() +""" + } + ] + + let actual = codeFix |> multiFix code Auto Assert.Equal(expected, actual) @@ -70,9 +110,9 @@ module Module2 = let song = { Titel = "Jigsaw Falling Into Place" } """ - let expected = None + let expected = [] - let actual = codeFix |> tryFix code Auto + let actual = codeFix |> multiFix code Auto Assert.Equal(expected, actual) @@ -83,9 +123,9 @@ let ``Doesn't fix FS0039 for random undefined stuff`` () = let f = g """ - let expected = None + let expected = [] - let actual = codeFix |> tryFix code Auto + let actual = codeFix |> multiFix code Auto Assert.Equal(expected, actual) @@ -100,7 +140,7 @@ let song = Song(titel = "Under The Milky Way") """ let expected = - Some + [ { Message = "Replace with 'title'" FixedCode = @@ -111,7 +151,8 @@ type Song(title: string) = let song = Song(title = "Under The Milky Way") """ } + ] - let actual = codeFix |> tryFix code Auto + let actual = codeFix |> multiFix code Auto Assert.Equal(expected, actual)