diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/AddInstanceMemberParameter.fs b/vsintegration/src/FSharp.Editor/CodeFixes/AddInstanceMemberParameter.fs index 4e757dbe617..21b732b05dc 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/AddInstanceMemberParameter.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/AddInstanceMemberParameter.fs @@ -21,12 +21,12 @@ type internal AddInstanceMemberParameterCodeFixProvider() = override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this) interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync _ span = + member _.GetCodeFixIfAppliesAsync context = let codeFix = { Name = CodeFix.AddInstanceMemberParameter Message = title - Changes = [ TextChange(TextSpan(span.Start, 0), "x.") ] + Changes = [ TextChange(TextSpan(context.Span.Start, 0), "x.") ] } CancellableTask.singleton (Some codeFix) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingFunKeyword.fs b/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingFunKeyword.fs index 65854292e79..921cb77f149 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingFunKeyword.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingFunKeyword.fs @@ -9,52 +9,63 @@ open System.Collections.Immutable open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open CancellableTasks + [] type internal AddMissingFunKeywordCodeFixProvider [] () = inherit CodeFixProvider() + static let title = SR.AddMissingFunKeyword() - override _.FixableDiagnosticIds = ImmutableArray.Create("FS0010") - - override _.RegisterCodeFixesAsync context = - asyncMaybe { - let document = context.Document - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) - let textOfError = sourceText.GetSubText(context.Span).ToString() - - // Only trigger when failing to parse `->`, which arises when `fun` is missing - do! Option.guard (textOfError = "->") - - let! defines, langVersion = - document.GetFSharpCompilationDefinesAndLangVersionAsync(nameof (AddMissingFunKeywordCodeFixProvider)) - |> liftAsync - - let adjustedPosition = - let rec loop ch pos = - if not (Char.IsWhiteSpace(ch)) then - pos - else - loop sourceText.[pos] (pos - 1) - - loop sourceText.[context.Span.Start - 1] context.Span.Start - - let! intendedArgLexerSymbol = - Tokenizer.getSymbolAtPosition ( - document.Id, - sourceText, - adjustedPosition, - document.FilePath, - defines, - SymbolLookupKind.Greedy, - false, - false, - Some langVersion, - context.CancellationToken - ) - - let! intendedArgSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range) - - do context.RegisterFsharpFix(CodeFix.AddMissingFunKeyword, title, [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |]) - } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + let adjustPosition (sourceText: SourceText) (span: TextSpan) = + let rec loop ch pos = + if not (Char.IsWhiteSpace(ch)) then + pos + else + loop sourceText[pos] (pos - 1) + + loop (sourceText[span.Start - 1]) span.Start + + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0010" + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + interface IFSharpCodeFixProvider with + member _.GetCodeFixIfAppliesAsync context = + cancellableTask { + let! textOfError = context.GetSquigglyTextAsync() + + if textOfError <> "->" then + return None + else + let! cancellationToken = CancellableTask.getCurrentCancellationToken () + let document = context.Document + + let! defines, langVersion = + document.GetFSharpCompilationDefinesAndLangVersionAsync(nameof AddMissingFunKeywordCodeFixProvider) + + let! sourceText = context.GetSourceTextAsync() + let adjustedPosition = adjustPosition sourceText context.Span + + return + Tokenizer.getSymbolAtPosition ( + document.Id, + sourceText, + adjustedPosition, + document.FilePath, + defines, + SymbolLookupKind.Greedy, + false, + false, + Some langVersion, + cancellationToken + ) + |> Option.bind (fun intendedArgLexerSymbol -> + RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range)) + |> Option.map (fun intendedArgSpan -> + { + Name = CodeFix.AddMissingFunKeyword + Message = title + Changes = [ TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") ] + }) + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingRecToMutuallyRecFunctions.fs b/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingRecToMutuallyRecFunctions.fs index 5713ca8e68c..5d510bf381b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingRecToMutuallyRecFunctions.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/AddMissingRecToMutuallyRecFunctions.fs @@ -22,14 +22,16 @@ type internal AddMissingRecToMutuallyRecFunctionsCodeFixProvider [") @@ -40,7 +37,7 @@ type internal ChangeToUpcastCodeFixProvider() = else text.Replace("downcast", "upcast") - let changes = [ TextChange(span, replacement) ] + let changes = [ TextChange(context.Span, replacement) ] let title = if isDowncastOperator then diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs index 13ce3fc3095..3b96ca8696f 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs @@ -83,8 +83,26 @@ module internal CodeFixExtensions = member ctx.RegisterFsharpFix(codeFix: IFSharpCodeFixProvider) = cancellableTask { - match! codeFix.GetCodeFixIfAppliesAsync ctx.Document ctx.Span with + match! codeFix.GetCodeFixIfAppliesAsync ctx with | Some codeFix -> ctx.RegisterFsharpFix(codeFix.Name, codeFix.Message, codeFix.Changes) | None -> () } |> CancellableTask.startAsTask ctx.CancellationToken + + member ctx.GetSourceTextAsync() = + cancellableTask { + let! cancellationToken = CancellableTask.getCurrentCancellationToken () + return! ctx.Document.GetTextAsync cancellationToken + } + + member ctx.GetSquigglyTextAsync() = + cancellableTask { + let! sourceText = ctx.GetSourceTextAsync() + return sourceText.GetSubText(ctx.Span).ToString() + } + + member ctx.GetErrorRangeAsync() = + cancellableTask { + let! sourceText = ctx.GetSourceTextAsync() + return RoslynHelpers.TextSpanToFSharpRange(ctx.Document.FilePath, ctx.Span, sourceText) + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs index 7028b9b5afe..b248d9083ec 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs @@ -32,21 +32,18 @@ type internal ConvertCSharpLambdaToFSharpLambdaCodeFixProvider [ flatten3 - override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039") + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0039" - override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this) + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync document span = + member _.GetCodeFixIfAppliesAsync context = cancellableTask { let! cancellationToken = CancellableTask.getCurrentCancellationToken () - let! parseResults = document.GetFSharpParseResultsAsync(nameof (ConvertCSharpLambdaToFSharpLambdaCodeFixProvider)) - - let! sourceText = document.GetTextAsync(cancellationToken) - - let errorRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText) + let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof ConvertCSharpLambdaToFSharpLambdaCodeFixProvider) + let! sourceText = context.Document.GetTextAsync(cancellationToken) + let! errorRange = context.GetErrorRangeAsync() return tryGetSpans parseResults errorRange sourceText @@ -54,7 +51,7 @@ type internal ConvertCSharpLambdaToFSharpLambdaCodeFixProvider [ " + bodyText) + TextChange(fullParenSpan, $"fun {argText} -> {bodyText}") { Name = CodeFix.ConvertCSharpLambdaToFSharpLambda diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertToAnonymousRecord.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertToAnonymousRecord.fs index 098045b9295..17e5653daf1 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ConvertToAnonymousRecord.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ConvertToAnonymousRecord.fs @@ -21,16 +21,11 @@ type internal ConvertToAnonymousRecordCodeFixProvider [] ( override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this) interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync document span = + member _.GetCodeFixIfAppliesAsync context = cancellableTask { - let! cancellationToken = CancellableTask.getCurrentCancellationToken () - - let! parseResults = document.GetFSharpParseResultsAsync(nameof (ConvertToAnonymousRecordCodeFixProvider)) - - let! sourceText = document.GetTextAsync(cancellationToken) - - let errorRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText) + let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof ConvertToAnonymousRecordCodeFixProvider) + let! sourceText = context.GetSourceTextAsync() + let! errorRange = context.GetErrorRangeAsync() return parseResults.TryRangeOfRecordExpressionContainingPos errorRange.Start diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs b/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs index 36a4ff2db56..ca1dbd8961f 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs @@ -2,7 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor -open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.Text open CancellableTasks @@ -15,4 +15,4 @@ type FSharpCodeFix = } type IFSharpCodeFixProvider = - abstract member GetCodeFixIfAppliesAsync: document: Document -> span: TextSpan -> CancellableTask + abstract member GetCodeFixIfAppliesAsync: context: CodeFixContext -> CancellableTask diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/MakeOuterBindingRecursive.fs b/vsintegration/src/FSharp.Editor/CodeFixes/MakeOuterBindingRecursive.fs index c0c245fe066..760b7339b37 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/MakeOuterBindingRecursive.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/MakeOuterBindingRecursive.fs @@ -9,45 +9,40 @@ open System.Collections.Immutable open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes +open CancellableTasks + [] type internal MakeOuterBindingRecursiveCodeFixProvider [] () = inherit CodeFixProvider() - override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039") - - override _.RegisterCodeFixesAsync context = - asyncMaybe { - let! parseResults = - context.Document.GetFSharpParseResultsAsync(nameof (MakeOuterBindingRecursiveCodeFixProvider)) - |> liftAsync - - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) - - let diagnosticRange = - RoslynHelpers.TextSpanToFSharpRange(context.Document.FilePath, context.Span, sourceText) - - do! Option.guard (parseResults.IsPosContainedInApplication diagnosticRange.Start) - - let! outerBindingRange = parseResults.TryRangeOfNameOfNearestOuterBindingContainingPos diagnosticRange.Start - let! outerBindingNameSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, outerBindingRange) - - // One last check to verify the names are the same - do! - Option.guard ( - sourceText - .GetSubText(outerBindingNameSpan) - .ContentEquals(sourceText.GetSubText(context.Span)) - ) - - let title = - String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(outerBindingNameSpan).ToString()) - - do - context.RegisterFsharpFix( - CodeFix.MakeOuterBindingRecursive, - title, - [| TextChange(TextSpan(outerBindingNameSpan.Start, 0), "rec ") |] - ) - } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0039" + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + interface IFSharpCodeFixProvider with + member _.GetCodeFixIfAppliesAsync context = + cancellableTask { + let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof MakeOuterBindingRecursiveCodeFixProvider) + let! sourceText = context.GetSourceTextAsync() + let! diagnosticRange = context.GetErrorRangeAsync() + + if not <| parseResults.IsPosContainedInApplication diagnosticRange.Start then + return None + else + return + parseResults.TryRangeOfNameOfNearestOuterBindingContainingPos diagnosticRange.Start + |> Option.bind (fun bindingRange -> RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, bindingRange)) + |> Option.filter (fun bindingSpan -> + sourceText + .GetSubText(bindingSpan) + .ContentEquals(sourceText.GetSubText context.Span)) + |> Option.map (fun bindingSpan -> + let title = + String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(bindingSpan).ToString()) + + { + Name = CodeFix.MakeOuterBindingRecursive + Message = title + Changes = [ TextChange(TextSpan(bindingSpan.Start, 0), "rec ") ] + }) + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ProposeUppercaseLabel.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ProposeUppercaseLabel.fs index ceaaa8bcbc3..2c8e7d7c188 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ProposeUppercaseLabel.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ProposeUppercaseLabel.fs @@ -2,32 +2,45 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Composition -open System.Threading.Tasks open System.Collections.Immutable open Microsoft.CodeAnalysis.CodeFixes -open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.Text open FSharp.Compiler.Diagnostics -[] -type internal ProposeUpperCaseLabelCodeFixProvider [] () = - inherit CodeFixProvider() - - override _.FixableDiagnosticIds = ImmutableArray.Create("FS0053") - - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - let textChanger (originalText: string) = - originalText.[0].ToString().ToUpper() + originalText.Substring(1) +open CancellableTasks - let! solutionChanger, originalText = SymbolHelpers.changeAllSymbolReferences (context.Document, context.Span, textChanger) - - let title = - CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion <| textChanger originalText) +[] +type internal ProposeUppercaseLabelCodeFixProvider [] () = + inherit CodeFixProvider() - context.RegisterCodeFix(CodeAction.Create(title, solutionChanger, title), context.Diagnostics) - } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0053" + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this + + interface IFSharpCodeFixProvider with + member _.GetCodeFixIfAppliesAsync context = + cancellableTask { + let! errorText = context.GetSquigglyTextAsync() + + // probably not the 100% robust way to do that + // but actually we could also just implement the code fix for this case as well + if errorText.StartsWith "exception " then + return None + else + let upperCased = string (Char.ToUpper errorText[0]) + errorText.Substring(1) + + let title = + CompilerDiagnostics.GetErrorMessage(FSharpDiagnosticKind.ReplaceWithSuggestion upperCased) + + return + (Some + { + Name = CodeFix.ProposeUppercaseLabel + Message = title + Changes = [ TextChange(TextSpan(context.Span.Start, context.Span.Length), upperCased) ] + }) + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveReturnOrYield.fs b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveReturnOrYield.fs index 728faa005ae..94d25ff593c 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/RemoveReturnOrYield.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/RemoveReturnOrYield.fs @@ -19,24 +19,20 @@ type internal RemoveReturnOrYieldCodeFixProvider [] () = override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this) interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync document span = + member _.GetCodeFixIfAppliesAsync context = cancellableTask { - let! cancellationToken = CancellableTask.getCurrentCancellationToken () + let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof RemoveReturnOrYieldCodeFixProvider) - let! parseResults = document.GetFSharpParseResultsAsync(nameof (RemoveReturnOrYieldCodeFixProvider)) - - let! sourceText = document.GetTextAsync(cancellationToken) - - let errorRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText) + let! sourceText = context.GetSourceTextAsync() + let! errorRange = context.GetErrorRangeAsync() return parseResults.TryRangeOfExprInYieldOrReturn errorRange.Start |> Option.bind (fun exprRange -> RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, exprRange)) - |> Option.map (fun exprSpan -> [ TextChange(span, sourceText.GetSubText(exprSpan).ToString()) ]) + |> Option.map (fun exprSpan -> [ TextChange(context.Span, sourceText.GetSubText(exprSpan).ToString()) ]) |> Option.map (fun changes -> let title = - let text = sourceText.GetSubText(span).ToString() + let text = sourceText.GetSubText(context.Span).ToString() if text.StartsWith("return!") then SR.RemoveReturnBang() elif text.StartsWith("return") then SR.RemoveReturn() diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/WrapExpressionInParentheses.fs b/vsintegration/src/FSharp.Editor/CodeFixes/WrapExpressionInParentheses.fs index 39a247263c3..b9d77613899 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/WrapExpressionInParentheses.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/WrapExpressionInParentheses.fs @@ -21,15 +21,15 @@ type internal WrapExpressionInParenthesesCodeFixProvider() = override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this) interface IFSharpCodeFixProvider with - member _.GetCodeFixIfAppliesAsync _ span = + member _.GetCodeFixIfAppliesAsync context = let codeFix = { Name = CodeFix.AddParentheses Message = title Changes = [ - TextChange(TextSpan(span.Start, 0), "(") - TextChange(TextSpan(span.End, 0), ")") + TextChange(TextSpan(context.Span.Start, 0), "(") + TextChange(TextSpan(context.Span.End, 0), ")") ] } diff --git a/vsintegration/src/FSharp.Editor/Common/Constants.fs b/vsintegration/src/FSharp.Editor/Common/Constants.fs index 9eef0df9fac..5e24d8a86b7 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -167,6 +167,9 @@ module internal CodeFix = [] let AddMissingFunKeyword = "AddMissingFunKeyword" + [] + let ProposeUppercaseLabel = "ProposeUppercaseLabel" + [] let AddNewKeyword = "AddNewKeyword" diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs index f84c2bec552..3be0f1ea218 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs @@ -2,14 +2,12 @@ namespace Microsoft.VisualStudio.FSharp.Editor -open System open System.Collections.Concurrent open System.Collections.Immutable open System.Threading open System.Threading.Tasks open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.Text open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols @@ -162,70 +160,3 @@ module internal SymbolHelpers = let usesByDocumentId = symbolUsesWithDocumentId |> Seq.groupBy fst return usesByDocumentId.ToImmutableDictionary(fst, snd >> Seq.map snd >> Seq.toArray) } - - type OriginalText = string - - // Note, this function is broken and shouldn't be used because the source text ranges to replace are applied sequentially, - // breaking the position computations as changes progress, especially if two changes are made on the same line. - // - // However, it is only currently used by ProposeUpperCaseLabel code fix, where the changes to code will rarely be on the same line. - // - // A better approach is to use something like createTextChangeCodeFix below, with a delayed function to compute a set of changes to be applied - // simultaneously. But that doesn't work for this case, as we want a set of changes to apply acrosss the whole solution. - - let changeAllSymbolReferences - ( - document: Document, - symbolSpan: TextSpan, - textChanger: string -> string - ) : Async<(Func> * OriginalText) option> = - asyncMaybe { - let userOpName = "changeAllSymbolReferences" - do! Option.guard (symbolSpan.Length > 0) - let! cancellationToken = liftAsync Async.CancellationToken - let! sourceText = document.GetTextAsync(cancellationToken) - let originalText = sourceText.ToString(symbolSpan) - do! Option.guard (originalText.Length > 0) - - let! symbol = document.TryFindFSharpLexerSymbolAsync(symbolSpan.Start, SymbolLookupKind.Greedy, false, false, userOpName) - let textLine = sourceText.Lines.GetLineFromPosition(symbolSpan.Start) - let textLinePos = sourceText.Lines.GetLinePosition(symbolSpan.Start) - let fcsTextLineNumber = Line.fromZ textLinePos.Line - - let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - - let! symbolUse = - checkFileResults.GetSymbolUseAtLocation( - fcsTextLineNumber, - symbol.Ident.idRange.EndColumn, - textLine.ToString(), - symbol.FullIsland - ) - - let newText = textChanger originalText - // defer finding all symbol uses throughout the solution - return - Func<_, _>(fun (cancellationToken: CancellationToken) -> - async { - let! symbolUsesByDocumentId = getSymbolUsesInSolution (symbolUse, checkFileResults, document) - - let mutable solution = document.Project.Solution - - for KeyValue (documentId, symbolUses) in symbolUsesByDocumentId do - let document = document.Project.Solution.GetDocument(documentId) - let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask - let mutable sourceText = sourceText - - for symbolUse in symbolUses do - match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse) with - | None -> () - | Some span -> - let textSpan = Tokenizer.fixupSpan (sourceText, span) - sourceText <- sourceText.Replace(textSpan, newText) - solution <- solution.WithDocumentText(documentId, sourceText) - - return solution - } - |> RoslynHelpers.StartAsyncAsTask cancellationToken), - originalText - } diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddMissingFunKeywordTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddMissingFunKeywordTests.fs new file mode 100644 index 00000000000..44f02147908 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddMissingFunKeywordTests.fs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.AddMissingFunKeywordTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = AddMissingFunKeywordCodeFixProvider() +let private diagnostic = 0010 // Unexpected symbol... + +[] +let ``Fixes FS0010 for missing fun keyword`` () = + let code = + """ +let gettingEven numbers = + numbers + |> Seq.filter (x -> x / 2 = 0) +""" + + let expected = + Some + { + Message = "Add missing 'fun' keyword" + FixedCode = + """ +let gettingEven numbers = + numbers + |> Seq.filter (fun x -> x / 2 = 0) +""" + } + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) + +[] +let ``Doesn't fix FS0010 for random unexpected symbols`` () = + let code = + """ += +""" + + let expected = None + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs index 542039ccc58..4fb17657a19 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/CodeFixTestFramework.fs @@ -2,17 +2,24 @@ module FSharp.Editor.Tests.CodeFixes.CodeFixTestFramework +open System +open System.Collections.Immutable open System.Threading open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open FSharp.Compiler.Diagnostics open FSharp.Editor.Tests.Helpers type TestCodeFix = { Message: string; FixedCode: string } +let mockAction = + Action>(fun _ _ -> ()) + let getRelevantDiagnostic (document: Document) errorNumber = cancellableTask { let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync "test" @@ -23,17 +30,29 @@ let getRelevantDiagnostic (document: Document) errorNumber = |> Seq.head } +let createTestCodeFixContext (code: string) (document: Document) (diagnostic: FSharpDiagnostic) = + cancellableTask { + let! cancellationToken = CancellableTask.getCurrentCancellationToken () + + let sourceText = SourceText.From code + + let location = + RoslynHelpers.RangeToLocation(diagnostic.Range, sourceText, document.FilePath) + + let roslynDiagnostic = RoslynHelpers.ConvertError(diagnostic, location) + + return CodeFixContext(document, roslynDiagnostic, mockAction, cancellationToken) + } + let tryFix (code: string) diagnostic (fixProvider: IFSharpCodeFixProvider) = cancellableTask { let sourceText = SourceText.From code let document = RoslynTestHelpers.GetFsDocument code let! diagnostic = getRelevantDiagnostic document diagnostic + let! context = createTestCodeFixContext code document diagnostic - let diagnosticSpan = - RoslynHelpers.FSharpRangeToTextSpan(sourceText, diagnostic.Range) - - let! result = fixProvider.GetCodeFixIfAppliesAsync document diagnosticSpan + let! result = fixProvider.GetCodeFixIfAppliesAsync context return (result diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/MakeOuterBindingRecursiveTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/MakeOuterBindingRecursiveTests.fs new file mode 100644 index 00000000000..4f438e38f1c --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/MakeOuterBindingRecursiveTests.fs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.MakeOuterBindingRecursiveTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = MakeOuterBindingRecursiveCodeFixProvider() +let private diagnostic = 0039 // Something is not defined... + +[] +let ``Fixes FS0039 for recursive functions`` () = + let code = + """ +let factorial n = + match n with + | 0 -> 1 + | _ -> n * factorial (n - 1) +""" + + let expected = + Some + { + Message = "Make 'factorial' recursive" + FixedCode = + """ +let rec factorial n = + match n with + | 0 -> 1 + | _ -> n * factorial (n - 1) +""" + } + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) + +[] +let ``Doesn't fix FS0039 for random undefined stuff`` () = + let code = + """ +let f = g +""" + + let expected = None + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ProposeUppercaseLabelTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ProposeUppercaseLabelTests.fs new file mode 100644 index 00000000000..be035f91a97 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ProposeUppercaseLabelTests.fs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.ProposeUppercaseLabelTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = ProposeUppercaseLabelCodeFixProvider() +let private diagnostic = 0053 // ... must be uppercase identifiers ... + +[] +let ``Fixes FS0053 for discriminated unions`` () = + let code = + """ +type MyNumber = number of int +""" + + let expected = + Some + { + Message = "Replace with 'Number'" + FixedCode = + """ +type MyNumber = Number of int +""" + } + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) + +[] +let ``Doesn't fix FS0053 for exceptions`` () = + let code = + """ +exception lowException of string +""" + + let expected = None + + let actual = codeFix |> tryFix code diagnostic + + Assert.Equal(expected, actual) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 47be31b4652..31467716ebb 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -40,6 +40,9 @@ + + +