diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 60e86c9eb96..7282aa2c4e2 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -99,6 +99,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 4b41551aac0..9c12f2a6519 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -352,6 +352,9 @@ Use live (unsaved) buffers for analysis Convert C# 'using' to F# 'open' + + Add explicit return type annotation + Remove unnecessary parentheses diff --git a/vsintegration/src/FSharp.Editor/Refactor/AddExplicitReturnType.fs b/vsintegration/src/FSharp.Editor/Refactor/AddExplicitReturnType.fs new file mode 100644 index 00000000000..c603939f9fe --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/AddExplicitReturnType.fs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Threading +open System.Threading.Tasks + +open FSharp.Compiler +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Symbols +open FSharp.Compiler.Text +open FSharp.Compiler.Syntax + +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.CodeActions +open CancellableTasks + +[] +type internal AddExplicitReturnType [] () = + inherit CodeRefactoringProvider() + + static member isValidMethodWithoutTypeAnnotation (funcOrValue: FSharpMemberOrFunctionOrValue) (symbolUse: FSharpSymbolUse) (parseFileResults:FSharpParseFileResults) = + let isLambdaIfFunction = + funcOrValue.IsFunction + && parseFileResults.IsBindingALambdaAtPosition symbolUse.Range.Start + + (not funcOrValue.IsValue || not isLambdaIfFunction) + && not (funcOrValue.ReturnParameter.Type.IsUnresolved) + && not (parseFileResults.IsTypeAnnotationGivenAtPosition symbolUse.Range.Start) + + + static member refactor (context:CodeRefactoringContext) (symbolUse:FSharpSymbolUse,memberFunc:FSharpMemberOrFunctionOrValue,symbolSpan) = + let typeString = memberFunc.FullType.FormatWithConstraints symbolUse.DisplayContext + let title = SR.AddExplicitReturnTypeAnnotation() + + let getChangedText (sourceText: SourceText) = + let debugInfo = $"{sourceText} : {typeString} : {symbolSpan}" + debugInfo + let sub = sourceText.ToString(symbolSpan) + + let newSub = + sub.Replace("=", $" :{memberFunc.ReturnParameter.Type.TypeDefinition.DisplayName}=") + + sourceText.Replace(symbolSpan, newSub) + + let codeActionFunc = (fun (cancellationToken: CancellationToken) -> + backgroundTask { + let! sourceText = context.Document.GetTextAsync(cancellationToken) + let changedText = getChangedText sourceText + return context.Document.WithText(changedText) + } + ) + let codeAction = + CodeAction.Create( + title, + codeActionFunc, + title + ) + + + do context.RegisterRefactoring(codeAction) + + override _.ComputeRefactoringsAsync context = + backgroundTask { + let document = context.Document + let position = context.Span.Start + let! sourceText = document.GetTextAsync() + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + + let! ct = Async.CancellationToken + + let! lexerSymbol = + document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, nameof (AddExplicitReturnType)) + + + let! (parseFileResults,checkFileResults) = + document.GetFSharpParseAndCheckResultsAsync(nameof (AddExplicitReturnType)) + |> CancellableTask.start ct + + let res = + lexerSymbol + |> Option.bind(fun lexer -> + checkFileResults.GetSymbolUseAtLocation( + fcsTextLineNumber, + lexer.Ident.idRange.EndColumn, + textLine.ToString(), + lexer.FullIsland + )) + |> Option.bind(fun symbolUse-> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as v + when AddExplicitReturnType.isValidMethodWithoutTypeAnnotation v symbolUse parseFileResults-> + Some (symbolUse,v) + | _ -> None + ) + |> Option.bind(fun (symbolUse,memberFunc)-> + match RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, symbolUse.Range) with + | Some span -> Some(symbolUse,memberFunc,span) + | None -> None + ) + |> Option.map(fun (symbolUse,memberFunc,textSpan) -> AddExplicitReturnType.refactor context (symbolUse,memberFunc,textSpan)) + + return res + } diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 77ce57f27e0..606768a9f64 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -12,6 +12,7 @@ + @@ -71,6 +72,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddExplicitReturnType.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddExplicitReturnType.fs new file mode 100644 index 00000000000..14495b8ce07 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddExplicitReturnType.fs @@ -0,0 +1,52 @@ +module FSharp.Editor.Tests.Refactors.AddExplicitReturnType + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit +open System +open System.Collections.Immutable +open System.Text.RegularExpressions + +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 + +open Microsoft.CodeAnalysis.CodeRefactorings +open NUnit.Framework + + +[] +let ``Refactor changes something`` () = + task { + let code = + """ + let sum a b = a + b + """ + + let! ct = Async.CancellationToken + + let sourceText = SourceText.From code + let document = RoslynTestHelpers.GetFsDocument code + let spanStart = code.IndexOf "sum" + let span = TextSpan(spanStart, 3) + + let mutable refactorContext = + CodeRefactoringContext(document, span, Action(fun a -> ()), ct) + + let refactorProvider = AddExplicitReturnType() + let expected = None + + do! refactorProvider.ComputeRefactoringsAsync refactorContext + let newText = match refactorContext.TextDocument.TryGetText() with + | true,result -> result + | false,_ -> sourceText + + let! text = refactorContext.TextDocument.GetTextAsync( ct) + Assert.AreNotEqual(sourceText.ToString(),newText.ToString(),"") + + () + }